puls-dev 0.2.7 → 0.2.9

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 (117) hide show
  1. package/dist/core/checker.js +71 -0
  2. package/dist/core/config.d.ts +6 -0
  3. package/dist/core/config.js +11 -1
  4. package/dist/core/context.d.ts +14 -0
  5. package/dist/core/context.js +2 -0
  6. package/dist/core/decorators.d.ts +4 -0
  7. package/dist/core/decorators.js +56 -30
  8. package/dist/core/hooks.d.ts +21 -0
  9. package/dist/core/hooks.js +116 -0
  10. package/dist/core/hooks.test.d.ts +1 -0
  11. package/dist/core/hooks.test.js +194 -0
  12. package/dist/core/multiregion.test.d.ts +1 -0
  13. package/dist/core/multiregion.test.js +87 -0
  14. package/dist/core/output.d.ts +2 -0
  15. package/dist/core/output.js +9 -2
  16. package/dist/core/parallel.test.d.ts +1 -0
  17. package/dist/core/parallel.test.js +215 -0
  18. package/dist/core/parser.d.ts +10 -0
  19. package/dist/core/parser.js +140 -0
  20. package/dist/core/parser.test.d.ts +1 -0
  21. package/dist/core/parser.test.js +117 -0
  22. package/dist/core/production.test.d.ts +1 -0
  23. package/dist/core/production.test.js +189 -0
  24. package/dist/core/provisioner.d.ts +4 -0
  25. package/dist/core/provisioner.js +123 -0
  26. package/dist/core/resource.d.ts +23 -0
  27. package/dist/core/resource.js +54 -0
  28. package/dist/core/retry.d.ts +9 -0
  29. package/dist/core/retry.js +28 -0
  30. package/dist/core/retry.test.d.ts +1 -0
  31. package/dist/core/retry.test.js +66 -0
  32. package/dist/core/secret.d.ts +41 -0
  33. package/dist/core/secret.js +105 -0
  34. package/dist/core/secret.test.d.ts +1 -0
  35. package/dist/core/secret.test.js +166 -0
  36. package/dist/core/stack.d.ts +4 -3
  37. package/dist/core/stack.js +322 -48
  38. package/dist/index.d.ts +3 -0
  39. package/dist/index.js +3 -0
  40. package/dist/providers/aws/api.js +97 -17
  41. package/dist/providers/aws/ec2.d.ts +51 -0
  42. package/dist/providers/aws/ec2.js +331 -0
  43. package/dist/providers/aws/ec2.test.d.ts +1 -0
  44. package/dist/providers/aws/ec2.test.js +281 -0
  45. package/dist/providers/aws/index.d.ts +4 -0
  46. package/dist/providers/aws/index.js +4 -0
  47. package/dist/providers/aws/route53.d.ts +1 -0
  48. package/dist/providers/aws/route53.js +15 -2
  49. package/dist/providers/aws/route53.test.js +47 -0
  50. package/dist/providers/aws/template.d.ts +34 -0
  51. package/dist/providers/aws/template.js +252 -0
  52. package/dist/providers/aws/template.test.d.ts +1 -0
  53. package/dist/providers/aws/template.test.js +208 -0
  54. package/dist/providers/do/api.d.ts +3 -1
  55. package/dist/providers/do/api.js +126 -27
  56. package/dist/providers/do/app.d.ts +26 -0
  57. package/dist/providers/do/app.js +124 -0
  58. package/dist/providers/do/app.test.d.ts +1 -0
  59. package/dist/providers/do/app.test.js +268 -0
  60. package/dist/providers/do/database.d.ts +44 -0
  61. package/dist/providers/do/database.js +208 -0
  62. package/dist/providers/do/database.test.d.ts +1 -0
  63. package/dist/providers/do/database.test.js +293 -0
  64. package/dist/providers/do/domain.d.ts +2 -0
  65. package/dist/providers/do/domain.js +30 -0
  66. package/dist/providers/do/domain.test.js +49 -0
  67. package/dist/providers/do/droplet.d.ts +9 -0
  68. package/dist/providers/do/droplet.js +146 -8
  69. package/dist/providers/do/droplet.test.js +228 -1
  70. package/dist/providers/do/firewall.d.ts +2 -1
  71. package/dist/providers/do/firewall.js +23 -9
  72. package/dist/providers/do/firewall.test.js +54 -0
  73. package/dist/providers/do/index.d.ts +11 -0
  74. package/dist/providers/do/index.js +8 -0
  75. package/dist/providers/do/spaces.d.ts +27 -0
  76. package/dist/providers/do/spaces.js +142 -0
  77. package/dist/providers/do/spaces.test.d.ts +1 -0
  78. package/dist/providers/do/spaces.test.js +180 -0
  79. package/dist/providers/do/spaces_api.d.ts +2 -0
  80. package/dist/providers/do/spaces_api.js +20 -0
  81. package/dist/providers/do/vpc.d.ts +30 -0
  82. package/dist/providers/do/vpc.js +128 -0
  83. package/dist/providers/do/vpc.test.d.ts +1 -0
  84. package/dist/providers/do/vpc.test.js +258 -0
  85. package/dist/providers/firebase/api.js +92 -29
  86. package/dist/providers/firebase/list.d.ts +2 -0
  87. package/dist/providers/firebase/list.js +25 -0
  88. package/dist/providers/gcp/api.js +88 -14
  89. package/dist/providers/gcp/clouddns.d.ts +1 -0
  90. package/dist/providers/gcp/clouddns.js +15 -2
  91. package/dist/providers/gcp/clouddns.test.js +45 -0
  92. package/dist/providers/gcp/index.d.ts +5 -1
  93. package/dist/providers/gcp/index.js +5 -1
  94. package/dist/providers/gcp/list.d.ts +2 -0
  95. package/dist/providers/gcp/list.js +55 -0
  96. package/dist/providers/gcp/secrets.js +1 -1
  97. package/dist/providers/gcp/template.d.ts +32 -0
  98. package/dist/providers/gcp/template.js +252 -0
  99. package/dist/providers/gcp/template.test.d.ts +1 -0
  100. package/dist/providers/gcp/template.test.js +227 -0
  101. package/dist/providers/gcp/vm.d.ts +48 -0
  102. package/dist/providers/gcp/vm.js +375 -0
  103. package/dist/providers/gcp/vm.test.d.ts +1 -0
  104. package/dist/providers/gcp/vm.test.js +321 -0
  105. package/dist/providers/proxmox/api.d.ts +1 -0
  106. package/dist/providers/proxmox/api.js +72 -16
  107. package/dist/providers/proxmox/index.d.ts +2 -0
  108. package/dist/providers/proxmox/index.js +2 -0
  109. package/dist/providers/proxmox/template.d.ts +44 -0
  110. package/dist/providers/proxmox/template.js +349 -0
  111. package/dist/providers/proxmox/template.test.d.ts +1 -0
  112. package/dist/providers/proxmox/template.test.js +179 -0
  113. package/dist/providers/proxmox/vm.d.ts +7 -4
  114. package/dist/providers/proxmox/vm.js +57 -102
  115. package/dist/providers/proxmox/vm.test.js +77 -0
  116. package/dist/types/inventory.d.ts +44 -1
  117. package/package.json +3 -1
@@ -1,7 +1,10 @@
1
1
  import fs from 'node:fs';
2
2
  import { GoogleAuth } from 'google-auth-library';
3
3
  import { Config } from '../../core/config.js';
4
+ import { withRetry } from '../../core/retry.js';
5
+ import { resourceContextStorage } from '../../core/context.js';
4
6
  function resolveFirebaseConfig() {
7
+ const isOffline = Config.isOfflineMode() || Config.isGlobalDryRun();
5
8
  const cfg = Config.get().providers.firebase;
6
9
  if (cfg?.serviceAccountPath)
7
10
  return cfg;
@@ -11,12 +14,18 @@ function resolveFirebaseConfig() {
11
14
  const sa = JSON.parse(fs.readFileSync(saPath, 'utf8'));
12
15
  return { projectId: sa.project_id, serviceAccountPath: saPath };
13
16
  }
17
+ if (isOffline) {
18
+ return { projectId: "mock-firebase-project", serviceAccountPath: "/mock/sa.json" };
19
+ }
14
20
  throw new Error('Firebase not configured. Set FIREBASE_SA=/path/to/sa.json or use @Deploy({ firebase: "..." })');
15
21
  }
16
22
  export function getProjectId() {
17
23
  return resolveFirebaseConfig().projectId;
18
24
  }
19
25
  export async function getFirebaseToken(scopes) {
26
+ if (Config.isOfflineMode() || Config.isGlobalDryRun()) {
27
+ return "mock-firebase-token";
28
+ }
20
29
  const { serviceAccountPath } = resolveFirebaseConfig();
21
30
  const auth = new GoogleAuth({ keyFile: serviceAccountPath, scopes });
22
31
  const client = await auth.getClient();
@@ -25,38 +34,92 @@ export async function getFirebaseToken(scopes) {
25
34
  }
26
35
  const HOSTING_SCOPE = 'https://www.googleapis.com/auth/firebase.hosting';
27
36
  const CLOUD_SCOPE = 'https://www.googleapis.com/auth/cloud-platform';
28
- export async function hostingFetch(path, opts = {}) {
29
- const token = await getFirebaseToken([HOSTING_SCOPE]);
30
- const base = 'https://firebasehosting.googleapis.com/v1beta1';
31
- const res = await fetch(`${base}${path}`, {
32
- ...opts,
33
- headers: {
34
- 'Authorization': `Bearer ${token}`,
35
- 'Content-Type': 'application/json',
36
- ...(opts.headers ?? {}),
37
- },
37
+ function createFirebaseOfflineMock(path, opts) {
38
+ if (path.includes("/versions")) {
39
+ return { name: `${path}/versions/mock-version-id`, status: "FINALIZED" };
40
+ }
41
+ if (path.includes("/releases")) {
42
+ return { name: `${path}/releases/mock-release-id` };
43
+ }
44
+ if (path.includes("/sites/")) {
45
+ return { name: "mock-site-name", defaultUrl: "https://mock-project.web.app" };
46
+ }
47
+ return new Proxy({}, {
48
+ get(target, prop) {
49
+ if (prop === "then")
50
+ return undefined;
51
+ if (prop === "name")
52
+ return "mock-firebase-name";
53
+ if (prop === "status")
54
+ return "FINALIZED";
55
+ if (prop === "id")
56
+ return "mock-firebase-id";
57
+ if (prop.endsWith("s"))
58
+ return [];
59
+ return `mock-fb-${prop.toLowerCase()}`;
60
+ }
38
61
  });
39
- if (!res.ok) {
40
- const body = await res.text();
41
- throw new Error(`Firebase Hosting API ${opts.method ?? 'GET'} ${path} → ${res.status}: ${body}`);
62
+ }
63
+ export async function hostingFetch(path, opts = {}) {
64
+ const context = resourceContextStorage.getStore();
65
+ const abortSignal = context?.abortSignal;
66
+ if (Config.isOfflineMode() || Config.isGlobalDryRun()) {
67
+ return Promise.resolve(createFirebaseOfflineMock(path, opts));
42
68
  }
43
- const text = await res.text();
44
- return text ? JSON.parse(text) : null;
69
+ const fetchOpts = abortSignal ? { ...opts, signal: abortSignal } : opts;
70
+ return withRetry(async () => {
71
+ const token = await getFirebaseToken([HOSTING_SCOPE]);
72
+ const base = 'https://firebasehosting.googleapis.com/v1beta1';
73
+ const res = await fetch(`${base}${path}`, {
74
+ ...fetchOpts,
75
+ headers: {
76
+ 'Authorization': `Bearer ${token}`,
77
+ 'Content-Type': 'application/json',
78
+ ...(fetchOpts.headers ?? {}),
79
+ },
80
+ });
81
+ if (!res.ok) {
82
+ const body = await res.text();
83
+ throw new Error(`Firebase Hosting API ${fetchOpts.method ?? 'GET'} ${path} → ${res.status}: ${body}`);
84
+ }
85
+ const text = await res.text();
86
+ return text ? JSON.parse(text) : null;
87
+ }, {
88
+ retryable: (err) => {
89
+ const match = err.message.match(/→ (\d+):/);
90
+ const status = match ? parseInt(match[1], 10) : null;
91
+ return status === 429 || (status && status >= 500) || err.message.includes("ETIMEDOUT");
92
+ }
93
+ });
45
94
  }
46
95
  export async function cloudFetch(base, path, opts = {}) {
47
- const token = await getFirebaseToken([CLOUD_SCOPE]);
48
- const res = await fetch(`${base}${path}`, {
49
- ...opts,
50
- headers: {
51
- 'Authorization': `Bearer ${token}`,
52
- 'Content-Type': 'application/json',
53
- ...(opts.headers ?? {}),
54
- },
55
- });
56
- if (!res.ok) {
57
- const body = await res.text();
58
- throw new Error(`GCP API ${opts.method ?? 'GET'} ${path} → ${res.status}: ${body}`);
96
+ const context = resourceContextStorage.getStore();
97
+ const abortSignal = context?.abortSignal;
98
+ if (Config.isOfflineMode() || Config.isGlobalDryRun()) {
99
+ return Promise.resolve(createFirebaseOfflineMock(path, opts));
59
100
  }
60
- const text = await res.text();
61
- return text ? JSON.parse(text) : null;
101
+ const fetchOpts = abortSignal ? { ...opts, signal: abortSignal } : opts;
102
+ return withRetry(async () => {
103
+ const token = await getFirebaseToken([CLOUD_SCOPE]);
104
+ const res = await fetch(`${base}${path}`, {
105
+ ...fetchOpts,
106
+ headers: {
107
+ 'Authorization': `Bearer ${token}`,
108
+ 'Content-Type': 'application/json',
109
+ ...(fetchOpts.headers ?? {}),
110
+ },
111
+ });
112
+ if (!res.ok) {
113
+ const body = await res.text();
114
+ throw new Error(`GCP API ${fetchOpts.method ?? 'GET'} ${path} → ${res.status}: ${body}`);
115
+ }
116
+ const text = await res.text();
117
+ return text ? JSON.parse(text) : null;
118
+ }, {
119
+ retryable: (err) => {
120
+ const match = err.message.match(/→ (\d+):/);
121
+ const status = match ? parseInt(match[1], 10) : null;
122
+ return status === 429 || (status && status >= 500) || err.message.includes("ETIMEDOUT");
123
+ }
124
+ });
62
125
  }
@@ -0,0 +1,2 @@
1
+ import type { FirebaseInventory } from "../../types/inventory.js";
2
+ export declare function listFirebaseResources(): Promise<FirebaseInventory>;
@@ -0,0 +1,25 @@
1
+ import { getProjectId, hostingFetch, cloudFetch } from "./api.js";
2
+ export async function listFirebaseResources() {
3
+ const project = getProjectId();
4
+ const [hostRes, fnRes] = await Promise.all([
5
+ hostingFetch(`/projects/${project}/sites`).catch(() => ({})),
6
+ cloudFetch("https://cloudfunctions.googleapis.com/v2", `/projects/${project}/locations/-/functions`).catch(() => ({})),
7
+ ]);
8
+ // 1. Map Hosting Sites
9
+ const hostingSites = (hostRes.sites ?? []).map((s) => ({
10
+ site: s.name.split("/").pop() ?? "unknown",
11
+ }));
12
+ // 2. Map Cloud Functions
13
+ const functions = (fnRes.functions ?? []).map((f) => {
14
+ const parts = f.name.split("/");
15
+ const name = parts.pop() ?? "unknown";
16
+ const region = parts[parts.indexOf("locations") + 1] ?? "unknown";
17
+ return {
18
+ name,
19
+ region,
20
+ entryPoint: f.buildConfig?.entryPoint ?? "unknown",
21
+ runtime: f.buildConfig?.runtime ?? "unknown",
22
+ };
23
+ });
24
+ return { hostingSites, functions };
25
+ }
@@ -1,7 +1,10 @@
1
1
  import fs from 'node:fs';
2
2
  import { GoogleAuth } from 'google-auth-library';
3
3
  import { Config } from '../../core/config.js';
4
+ import { withRetry } from '../../core/retry.js';
5
+ import { resourceContextStorage } from '../../core/context.js';
4
6
  export function resolveGCPConfig() {
7
+ const isOffline = Config.isOfflineMode() || Config.isGlobalDryRun();
5
8
  // 1. Check Config.providers.gcp
6
9
  const gcpCfg = Config.get().providers.gcp;
7
10
  if (gcpCfg?.serviceAccountPath) {
@@ -72,6 +75,13 @@ export function resolveGCPConfig() {
72
75
  // Continue to next fallback
73
76
  }
74
77
  }
78
+ if (isOffline) {
79
+ return {
80
+ projectId: "mock-gcp-project",
81
+ serviceAccountPath: "/mock/sa.json",
82
+ region: gcpCfg?.region ?? "us-central1"
83
+ };
84
+ }
75
85
  throw new Error('GCP credentials not configured. Please set GCP_SA or FIREBASE_SA env var, or configure providers.gcp or providers.firebase in Config.');
76
86
  }
77
87
  export function getProjectId() {
@@ -82,6 +92,9 @@ export function getRegion() {
82
92
  return gcpCfg?.region ?? 'us-central1';
83
93
  }
84
94
  export async function getGCPToken(scopes) {
95
+ if (Config.isOfflineMode() || Config.isGlobalDryRun()) {
96
+ return "mock-gcp-token";
97
+ }
85
98
  const { serviceAccountPath } = resolveGCPConfig();
86
99
  const auth = new GoogleAuth({ keyFile: serviceAccountPath, scopes });
87
100
  const client = await auth.getClient();
@@ -91,21 +104,82 @@ export async function getGCPToken(scopes) {
91
104
  }
92
105
  return token.token;
93
106
  }
107
+ function createGcpOfflineMock(base, path, opts) {
108
+ if (path.includes("/secrets/")) {
109
+ return {
110
+ payload: {
111
+ data: Buffer.from("mock-gcp-secret-value").toString("base64")
112
+ }
113
+ };
114
+ }
115
+ if (path.includes("/instances")) {
116
+ return {
117
+ status: "RUNNING",
118
+ id: "mock-gcp-instance-id",
119
+ networkInterfaces: [
120
+ {
121
+ networkIP: "10.128.0.2",
122
+ accessConfigs: [
123
+ {
124
+ natIP: "34.56.78.90"
125
+ }
126
+ ]
127
+ }
128
+ ]
129
+ };
130
+ }
131
+ if (path.includes("/global/networks")) {
132
+ return { status: "READY", name: "mock-network" };
133
+ }
134
+ if (path.includes("/subnetworks")) {
135
+ return { status: "READY", name: "mock-subnetwork" };
136
+ }
137
+ // Generic fallback proxy
138
+ return new Proxy({}, {
139
+ get(target, prop) {
140
+ if (prop === "then")
141
+ return undefined;
142
+ if (prop === "id")
143
+ return "mock-gcp-id-12345";
144
+ if (prop === "name")
145
+ return "mock-gcp-name";
146
+ if (prop === "status" || prop === "status")
147
+ return "RUNNING";
148
+ if (prop.endsWith("s"))
149
+ return [];
150
+ return `mock-gcp-${prop.toLowerCase()}`;
151
+ }
152
+ });
153
+ }
94
154
  const CLOUD_SCOPE = 'https://www.googleapis.com/auth/cloud-platform';
95
155
  export async function gcpFetch(base, path, opts = {}) {
96
- const token = await getGCPToken([CLOUD_SCOPE]);
97
- const res = await fetch(`${base}${path}`, {
98
- ...opts,
99
- headers: {
100
- 'Authorization': `Bearer ${token}`,
101
- 'Content-Type': 'application/json',
102
- ...(opts.headers ?? {}),
103
- },
104
- });
105
- if (!res.ok) {
106
- const body = await res.text();
107
- throw new Error(`GCP API ${opts.method ?? 'GET'} ${path} → ${res.status}: ${body}`);
156
+ const context = resourceContextStorage.getStore();
157
+ const abortSignal = context?.abortSignal;
158
+ if (Config.isOfflineMode() || Config.isGlobalDryRun()) {
159
+ return Promise.resolve(createGcpOfflineMock(base, path, opts));
108
160
  }
109
- const text = await res.text();
110
- return text ? JSON.parse(text) : null;
161
+ const fetchOpts = abortSignal ? { ...opts, signal: abortSignal } : opts;
162
+ return withRetry(async () => {
163
+ const token = await getGCPToken([CLOUD_SCOPE]);
164
+ const res = await fetch(`${base}${path}`, {
165
+ ...fetchOpts,
166
+ headers: {
167
+ 'Authorization': `Bearer ${token}`,
168
+ 'Content-Type': 'application/json',
169
+ ...(fetchOpts.headers ?? {}),
170
+ },
171
+ });
172
+ if (!res.ok) {
173
+ const body = await res.text();
174
+ throw new Error(`GCP API ${fetchOpts.method ?? 'GET'} ${path} → ${res.status}: ${body}`);
175
+ }
176
+ const text = await res.text();
177
+ return text ? JSON.parse(text) : null;
178
+ }, {
179
+ retryable: (err) => {
180
+ const match = err.message.match(/→ (\d+):/);
181
+ const status = match ? parseInt(match[1], 10) : null;
182
+ return status === 429 || (status && status >= 500) || err.message.includes("ETIMEDOUT");
183
+ }
184
+ });
111
185
  }
@@ -19,6 +19,7 @@ export declare class GCPCloudDNSZoneBuilder extends BaseBuilder {
19
19
  private records;
20
20
  constructor(zoneName: string);
21
21
  private discoverZone;
22
+ record(filePath: string): this;
22
23
  record(name: string, type: "A" | "AAAA" | "CNAME" | "MX" | "TXT" | "NS" | "PTR" | "SRV" | "CAA" | "SPF", value: string, ttl?: number): this;
23
24
  pointer(name: string, target: BaseBuilder | Output<string> | string): this;
24
25
  deploy(): Promise<{
@@ -1,6 +1,7 @@
1
1
  import { BaseBuilder } from "../../core/resource.js";
2
2
  import { Output } from "../../core/output.js";
3
3
  import { gcpFetch, getProjectId } from "./api.js";
4
+ import { loadRecordsFromFile } from "../../core/parser.js";
4
5
  const DNS_BASE = "https://dns.googleapis.com";
5
6
  function cleanZoneId(domain) {
6
7
  return domain
@@ -57,8 +58,20 @@ export class GCPCloudDNSZoneBuilder extends BaseBuilder {
57
58
  throw e;
58
59
  }
59
60
  }
60
- record(name, type, value, ttl = 300) {
61
- this.records.push({ name, type, value, ttl });
61
+ record(nameOrPath, type, value, ttl = 300) {
62
+ if (arguments.length === 1 && typeof nameOrPath === "string" && (nameOrPath.endsWith(".yaml") || nameOrPath.endsWith(".yml") || nameOrPath.endsWith(".json"))) {
63
+ const loaded = loadRecordsFromFile(nameOrPath);
64
+ for (const r of loaded) {
65
+ this.records.push({
66
+ name: r.name,
67
+ type: r.type,
68
+ value: r.value,
69
+ ttl: r.ttl ?? 300,
70
+ });
71
+ }
72
+ return this;
73
+ }
74
+ this.records.push({ name: nameOrPath, type: type, value: value, ttl });
62
75
  return this;
63
76
  }
64
77
  pointer(name, target) {
@@ -1,5 +1,7 @@
1
1
  import { test, describe, beforeEach, afterEach, mock } from "node:test";
2
2
  import assert from "node:assert";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
3
5
  import { GoogleAuth } from "google-auth-library";
4
6
  import { GCPCloudDNSZoneBuilder } from "./clouddns.js";
5
7
  import { Config } from "../../core/config.js";
@@ -256,4 +258,47 @@ describe("GCPCloudDNSZoneBuilder Unit Tests", () => {
256
258
  const deleteCall = fetchCalls.find((c) => c.method === "DELETE" && c.url.endsWith("/managedZones/to-delete-com"));
257
259
  assert.ok(deleteCall);
258
260
  });
261
+ test("loads records from a configuration file (YAML) successfully", async () => {
262
+ // 1. Zone exists
263
+ mockResponses["GET /managedZones/file-zone-com"] = {
264
+ status: 200,
265
+ body: { name: "file-zone-com" },
266
+ };
267
+ mockResponses["GET /managedZones/file-zone-com/rrsets"] = { status: 200, body: { rrsets: [] } };
268
+ mockResponses["POST /managedZones/file-zone-com/changes"] = { status: 200, body: {} };
269
+ // 2. Mock YAML file creation
270
+ const tempYamlPath = path.resolve(process.cwd(), "temp-dns-records.yaml");
271
+ const yamlContent = `
272
+ - name: www
273
+ type: CNAME
274
+ value: lb.google.com
275
+ - name: mail
276
+ type: A
277
+ value: 1.2.3.4
278
+ ttl: 600
279
+ `;
280
+ fs.writeFileSync(tempYamlPath, yamlContent, "utf-8");
281
+ try {
282
+ const builder = new GCPCloudDNSZoneBuilder("file-zone.com")
283
+ .record("temp-dns-records.yaml")
284
+ .record("api", "A", "10.0.0.9", 120); // Hybrid programmatic record!
285
+ const result = await builder.deploy();
286
+ assert.strictEqual(result.records.length, 3);
287
+ const wwwRec = result.records.find((r) => r.name === "www.file-zone.com.");
288
+ assert.ok(wwwRec);
289
+ assert.strictEqual(wwwRec.type, "CNAME");
290
+ assert.deepStrictEqual(wwwRec.rrdatas, ["lb.google.com."]);
291
+ const mailRec = result.records.find((r) => r.name === "mail.file-zone.com.");
292
+ assert.ok(mailRec);
293
+ assert.strictEqual(mailRec.type, "A");
294
+ assert.strictEqual(mailRec.ttl, 600);
295
+ const apiRec = result.records.find((r) => r.name === "api.file-zone.com.");
296
+ assert.ok(apiRec);
297
+ assert.strictEqual(apiRec.ttl, 120);
298
+ }
299
+ finally {
300
+ if (fs.existsSync(tempYamlPath))
301
+ fs.unlinkSync(tempYamlPath);
302
+ }
303
+ });
259
304
  });
@@ -4,7 +4,9 @@ import { GCPSecretBuilder, resolveGCPEnvVars } from './secrets.js';
4
4
  import { GCPPubSubTopicBuilder, GCPPubSubSubscriptionBuilder } from './pubsub.js';
5
5
  import { GCPCloudDNSZoneBuilder } from './clouddns.js';
6
6
  import { GCPServiceAccountBuilder, GCPIAMBindingBuilder } from './iam.js';
7
- export { GCPSecretBuilder, resolveGCPEnvVars, GCPPubSubTopicBuilder, GCPPubSubSubscriptionBuilder, GCPCloudDNSZoneBuilder, GCPServiceAccountBuilder, GCPIAMBindingBuilder };
7
+ import { GCPVMBuilder } from './vm.js';
8
+ import { GCPTemplateBuilder } from './template.js';
9
+ export { GCPSecretBuilder, resolveGCPEnvVars, GCPPubSubTopicBuilder, GCPPubSubSubscriptionBuilder, GCPCloudDNSZoneBuilder, GCPServiceAccountBuilder, GCPIAMBindingBuilder, GCPVMBuilder, GCPTemplateBuilder };
8
10
  export declare const GCP: {
9
11
  CloudRun: (serviceId: string) => GCPCloudRunBuilder;
10
12
  CloudSQL: (instanceId: string) => GCPCloudSQLBuilder;
@@ -16,4 +18,6 @@ export declare const GCP: {
16
18
  Topic: (topicId: string) => GCPPubSubTopicBuilder;
17
19
  Subscription: (subscriptionId: string) => GCPPubSubSubscriptionBuilder;
18
20
  };
21
+ VM: (instanceId: string) => GCPVMBuilder;
22
+ Template: (instanceId: string) => GCPTemplateBuilder;
19
23
  };
@@ -4,7 +4,9 @@ import { GCPSecretBuilder, resolveGCPEnvVars } from './secrets.js';
4
4
  import { GCPPubSubTopicBuilder, GCPPubSubSubscriptionBuilder } from './pubsub.js';
5
5
  import { GCPCloudDNSZoneBuilder } from './clouddns.js';
6
6
  import { GCPServiceAccountBuilder, GCPIAMBindingBuilder } from './iam.js';
7
- export { GCPSecretBuilder, resolveGCPEnvVars, GCPPubSubTopicBuilder, GCPPubSubSubscriptionBuilder, GCPCloudDNSZoneBuilder, GCPServiceAccountBuilder, GCPIAMBindingBuilder };
7
+ import { GCPVMBuilder } from './vm.js';
8
+ import { GCPTemplateBuilder } from './template.js';
9
+ export { GCPSecretBuilder, resolveGCPEnvVars, GCPPubSubTopicBuilder, GCPPubSubSubscriptionBuilder, GCPCloudDNSZoneBuilder, GCPServiceAccountBuilder, GCPIAMBindingBuilder, GCPVMBuilder, GCPTemplateBuilder };
8
10
  export const GCP = {
9
11
  CloudRun: (serviceId) => new GCPCloudRunBuilder(serviceId),
10
12
  CloudSQL: (instanceId) => new GCPCloudSQLBuilder(instanceId),
@@ -16,4 +18,6 @@ export const GCP = {
16
18
  Topic: (topicId) => new GCPPubSubTopicBuilder(topicId),
17
19
  Subscription: (subscriptionId) => new GCPPubSubSubscriptionBuilder(subscriptionId),
18
20
  },
21
+ VM: (instanceId) => new GCPVMBuilder(instanceId),
22
+ Template: (instanceId) => new GCPTemplateBuilder(instanceId),
19
23
  };
@@ -0,0 +1,2 @@
1
+ import type { GcpInventory } from "../../types/inventory.js";
2
+ export declare function listGcpResources(): Promise<GcpInventory>;
@@ -0,0 +1,55 @@
1
+ import { gcpFetch, getProjectId } from "./api.js";
2
+ export async function listGcpResources() {
3
+ const project = getProjectId();
4
+ const [vmRes, sqlRes, runRes, dnsRes] = await Promise.all([
5
+ gcpFetch("https://compute.googleapis.com", `/compute/v1/projects/${project}/aggregated/instances`).catch(() => ({})),
6
+ gcpFetch("https://sqladmin.googleapis.com", `/v1/projects/${project}/instances`).catch(() => ({})),
7
+ gcpFetch("https://run.googleapis.com", `/v2/projects/${project}/locations/-/services`).catch(() => ({})),
8
+ gcpFetch("https://dns.googleapis.com", `/dns/v1/projects/${project}/managedZones`).catch(() => ({})),
9
+ ]);
10
+ // 1. Map VM Instances
11
+ const vms = [];
12
+ if (vmRes.items) {
13
+ for (const [zoneKey, zoneData] of Object.entries(vmRes.items)) {
14
+ const data = zoneData;
15
+ if (data.instances) {
16
+ const zone = zoneKey.split("/").pop() ?? zoneKey;
17
+ for (const inst of data.instances) {
18
+ const machineType = inst.machineType?.split("/").pop() ?? "unknown";
19
+ const ip = inst.networkInterfaces?.[0]?.accessConfigs?.[0]?.natIP ?? "no-ip";
20
+ vms.push({
21
+ name: inst.name,
22
+ zone,
23
+ machineType,
24
+ status: inst.status ?? "unknown",
25
+ ip,
26
+ });
27
+ }
28
+ }
29
+ }
30
+ }
31
+ // 2. Map Cloud SQL Instances
32
+ const rdsInstances = (sqlRes.items ?? []).map((i) => ({
33
+ name: i.name,
34
+ engine: i.databaseVersion ?? "unknown",
35
+ tier: i.settings?.tier ?? "unknown",
36
+ status: i.state ?? "unknown",
37
+ }));
38
+ // 3. Map Cloud Run Services
39
+ const distributions = (runRes.services ?? []).map((s) => {
40
+ const parts = s.name.split("/");
41
+ const name = parts.pop() ?? "unknown";
42
+ const region = parts[parts.indexOf("locations") + 1] ?? "unknown";
43
+ return {
44
+ name,
45
+ region,
46
+ url: s.uri ?? "no-url",
47
+ };
48
+ });
49
+ // 4. Map Cloud DNS Zones
50
+ const hostedZones = (dnsRes.managedZones ?? []).map((z) => ({
51
+ name: z.name,
52
+ dnsName: z.dnsName ?? "",
53
+ }));
54
+ return { vms, rdsInstances, distributions, hostedZones };
55
+ }
@@ -23,7 +23,7 @@ export class GCPSecretBuilder extends BaseBuilder {
23
23
  }
24
24
  }
25
25
  catch (err) {
26
- // If version access fails (e.g. no versions created yet), keep resolvedValue as null
26
+ console.warn(` ⚠️ Could not fetch latest version of secret "${secretId}": ${err.message}`);
27
27
  }
28
28
  return secret;
29
29
  }
@@ -0,0 +1,32 @@
1
+ import { BaseBuilder } from "../../core/resource.js";
2
+ import { Output } from "../../core/output.js";
3
+ export declare class GCPTemplateBuilder extends BaseBuilder {
4
+ readonly out: {
5
+ imageId: Output<string>;
6
+ };
7
+ private _baseImage;
8
+ private _machineType;
9
+ private _zone;
10
+ private _network;
11
+ private _sshKeys;
12
+ private _provision;
13
+ constructor(name: string);
14
+ baseImage(img: string): this;
15
+ machineType(type: string): this;
16
+ zone(z: string): this;
17
+ network(netPath: string): this;
18
+ sshKey(keys: string | string[]): this;
19
+ provision(...playbookPaths: (string | string[])[]): this;
20
+ private discoverImage;
21
+ protected checkPort(ip: string, port: number): Promise<boolean>;
22
+ protected runProvisioner(ip: string, script: string): Promise<void>;
23
+ deploy(): Promise<{
24
+ name: string;
25
+ imageId: string;
26
+ }>;
27
+ destroy(): Promise<{
28
+ destroyed: boolean;
29
+ } | {
30
+ destroyed: string;
31
+ }>;
32
+ }