puls-dev 0.3.4 → 0.3.5

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 (46) hide show
  1. package/dist/bin/install-shell.d.ts +2 -0
  2. package/dist/bin/install-shell.js +136 -0
  3. package/dist/bin/puls.js +32 -10
  4. package/dist/core/checker.js +74 -0
  5. package/dist/core/decorators.js +17 -1
  6. package/dist/core/resource.d.ts +35 -0
  7. package/dist/core/resource.js +57 -1
  8. package/dist/core/stack.d.ts +11 -0
  9. package/dist/core/stack.js +88 -1
  10. package/dist/index.d.ts +1 -0
  11. package/dist/providers/aws/api.js +3 -0
  12. package/dist/providers/aws/ec2.d.ts +5 -0
  13. package/dist/providers/aws/ec2.js +7 -0
  14. package/dist/providers/aws/lambda.d.ts +5 -0
  15. package/dist/providers/aws/lambda.js +24 -0
  16. package/dist/providers/aws/list.js +15 -3
  17. package/dist/providers/aws/rds.d.ts +9 -0
  18. package/dist/providers/aws/rds.js +19 -0
  19. package/dist/providers/do/database.d.ts +9 -0
  20. package/dist/providers/do/database.js +19 -0
  21. package/dist/providers/do/domain.js +1 -1
  22. package/dist/providers/do/droplet.d.ts +5 -0
  23. package/dist/providers/do/droplet.js +10 -0
  24. package/dist/providers/do/list.js +25 -2
  25. package/dist/providers/do/load_balancer.d.ts +5 -0
  26. package/dist/providers/do/load_balancer.js +7 -0
  27. package/dist/providers/do/vpc.d.ts +5 -0
  28. package/dist/providers/do/vpc.js +8 -0
  29. package/dist/providers/firebase/functions.d.ts +9 -0
  30. package/dist/providers/firebase/functions.js +28 -0
  31. package/dist/providers/firebase/list.js +34 -2
  32. package/dist/providers/gcp/api.js +6 -0
  33. package/dist/providers/gcp/cloudrun.d.ts +13 -0
  34. package/dist/providers/gcp/cloudrun.js +30 -0
  35. package/dist/providers/gcp/cloudsql.d.ts +9 -0
  36. package/dist/providers/gcp/cloudsql.js +20 -0
  37. package/dist/providers/gcp/list.js +12 -2
  38. package/dist/providers/gcp/vm.d.ts +5 -0
  39. package/dist/providers/gcp/vm.js +8 -0
  40. package/dist/providers/proxmox/list.js +8 -1
  41. package/dist/providers/proxmox/vm.d.ts +13 -0
  42. package/dist/providers/proxmox/vm.js +16 -0
  43. package/dist/types/diff.d.ts +17 -0
  44. package/dist/types/diff.js +1 -0
  45. package/dist/types/inventory.d.ts +65 -0
  46. package/package.json +2 -2
@@ -67,6 +67,13 @@ export class EC2VMBuilder extends BaseBuilder {
67
67
  this._forceConfigCheck = true;
68
68
  return this;
69
69
  }
70
+ getDiff(existing) {
71
+ const diffs = [];
72
+ if (existing.InstanceType !== this._instanceType) {
73
+ diffs.push({ field: "instanceType", declared: this._instanceType, live: existing.InstanceType });
74
+ }
75
+ return diffs;
76
+ }
70
77
  async checkPort(ip, port) {
71
78
  return checkPort(ip, port);
72
79
  }
@@ -20,6 +20,11 @@ export declare class LambdaBuilder extends BaseBuilder {
20
20
  timeout(seconds: number): this;
21
21
  role(arnOrBuilder: string | IAMRoleBuilder): this;
22
22
  env(vars: Record<string, string | SecretsBuilder>): this;
23
+ getDiff(existing: any): {
24
+ field: string;
25
+ declared: string;
26
+ live: any;
27
+ }[];
23
28
  private ensureRole;
24
29
  private buildZip;
25
30
  deploy(): Promise<{
@@ -78,6 +78,30 @@ export class LambdaBuilder extends BaseBuilder {
78
78
  this._env = { ...this._env, ...vars };
79
79
  return this;
80
80
  }
81
+ getDiff(existing) {
82
+ const diffs = [];
83
+ if (existing.Runtime !== this._runtime) {
84
+ diffs.push({ field: "runtime", declared: this._runtime, live: existing.Runtime });
85
+ }
86
+ if (existing.Handler !== this._handler) {
87
+ diffs.push({ field: "handler", declared: this._handler, live: existing.Handler });
88
+ }
89
+ if (existing.MemorySize !== this._memory) {
90
+ diffs.push({ field: "memory", declared: `${this._memory} MB`, live: `${existing.MemorySize} MB` });
91
+ }
92
+ if (existing.Timeout !== this._timeout) {
93
+ diffs.push({ field: "timeout", declared: `${this._timeout}s`, live: `${existing.Timeout}s` });
94
+ }
95
+ const liveEnv = existing.Environment?.Variables ?? {};
96
+ const declaredKeys = Object.keys(this._env);
97
+ const liveKeys = Object.keys(liveEnv);
98
+ const allKeys = new Set([...declaredKeys, ...liveKeys]);
99
+ const envDrift = [...allKeys].filter((k) => String(this._env[k]) !== String(liveEnv[k]));
100
+ if (envDrift.length > 0) {
101
+ diffs.push({ field: "env", declared: `${declaredKeys.length} vars`, live: `${liveKeys.length} vars (${envDrift.length} changed)` });
102
+ }
103
+ return diffs;
104
+ }
81
105
  async ensureRole() {
82
106
  if (this._roleBuilder) {
83
107
  return await this._roleBuilder.out.arn.get();
@@ -3,16 +3,18 @@ import { ListBucketsCommand } from '@aws-sdk/client-s3';
3
3
  import { ListFunctionsCommand } from '@aws-sdk/client-lambda';
4
4
  import { DescribeDBInstancesCommand } from '@aws-sdk/client-rds';
5
5
  import { ListHostedZonesCommand } from '@aws-sdk/client-route-53';
6
- import { getCFClient, getS3Client, getLambdaClient, getRDSClient, getR53Client } from './api.js';
6
+ import { DescribeInstancesCommand } from '@aws-sdk/client-ec2';
7
+ import { getCFClient, getS3Client, getLambdaClient, getRDSClient, getR53Client, getEC2Client } from './api.js';
7
8
  import { Config } from '../../core/config.js';
8
9
  export async function listAwsResources() {
9
10
  const region = Config.get().providers.aws.region;
10
- const [cfResult, s3Result, lambdaResult, rdsResult, r53Result] = await Promise.all([
11
+ const [cfResult, s3Result, lambdaResult, rdsResult, r53Result, ec2Result] = await Promise.all([
11
12
  getCFClient().send(new ListDistributionsCommand({})),
12
13
  getS3Client().send(new ListBucketsCommand({})),
13
14
  getLambdaClient().send(new ListFunctionsCommand({ MaxItems: 50 })),
14
15
  getRDSClient().send(new DescribeDBInstancesCommand({})),
15
16
  getR53Client().send(new ListHostedZonesCommand({})),
17
+ getEC2Client().send(new DescribeInstancesCommand({ MaxResults: 200 })),
16
18
  ]);
17
19
  const distributions = (cfResult.DistributionList?.Items ?? []).map((d) => ({
18
20
  id: d.Id,
@@ -40,5 +42,15 @@ export async function listAwsResources() {
40
42
  id: z.Id.replace('/hostedzone/', ''),
41
43
  recordCount: z.ResourceRecordSetCount ?? 0,
42
44
  }));
43
- return { region, distributions, buckets, lambdas, rdsInstances, hostedZones };
45
+ const ec2Instances = (ec2Result.Reservations ?? [])
46
+ .flatMap((r) => r.Instances ?? [])
47
+ .filter((i) => i.State?.Name !== 'terminated')
48
+ .map((i) => ({
49
+ id: i.InstanceId,
50
+ name: i.Tags?.find((t) => t.Key === 'Name')?.Value ?? i.InstanceId,
51
+ type: i.InstanceType ?? 'unknown',
52
+ state: i.State?.Name ?? 'unknown',
53
+ publicIp: i.PublicIpAddress,
54
+ }));
55
+ return { region, distributions, buckets, lambdas, rdsInstances, hostedZones, ec2Instances };
44
56
  }
@@ -24,6 +24,15 @@ export declare class RDSBuilder extends BaseBuilder {
24
24
  subnets(ids: string[]): this;
25
25
  securityGroups(ids: string[]): this;
26
26
  publicAccess(enabled?: boolean): this;
27
+ getDiff(existing: any): ({
28
+ field: string;
29
+ declared: string;
30
+ live: any;
31
+ } | {
32
+ field: string;
33
+ declared: boolean;
34
+ live: any;
35
+ })[];
27
36
  database(name: string): this;
28
37
  credentials(username: string, password: string): this;
29
38
  private discoverInstance;
@@ -54,6 +54,25 @@ export class RDSBuilder extends BaseBuilder {
54
54
  this._publicAccess = enabled;
55
55
  return this;
56
56
  }
57
+ getDiff(existing) {
58
+ const diffs = [];
59
+ if (existing.Engine !== this._engine) {
60
+ diffs.push({ field: "engine", declared: this._engine, live: existing.Engine });
61
+ }
62
+ if (existing.EngineVersion !== this._engineVersion) {
63
+ diffs.push({ field: "engineVersion", declared: this._engineVersion, live: existing.EngineVersion });
64
+ }
65
+ if (existing.DBInstanceClass !== this._instanceClass) {
66
+ diffs.push({ field: "instanceClass", declared: this._instanceClass, live: existing.DBInstanceClass });
67
+ }
68
+ if (existing.AllocatedStorage !== this._storage) {
69
+ diffs.push({ field: "storage", declared: `${this._storage} GB`, live: `${existing.AllocatedStorage} GB` });
70
+ }
71
+ if (existing.PubliclyAccessible !== this._publicAccess) {
72
+ diffs.push({ field: "publicAccess", declared: this._publicAccess, live: existing.PubliclyAccessible });
73
+ }
74
+ return diffs;
75
+ }
57
76
  database(name) {
58
77
  this._dbName = name;
59
78
  return this;
@@ -26,6 +26,15 @@ export declare class DatabaseBuilder extends BaseBuilder {
26
26
  allowIp(cidr: string): this;
27
27
  allowDroplet(dropletId: string): this;
28
28
  allowTag(tagName: string): this;
29
+ getDiff(existing: any): ({
30
+ field: string;
31
+ declared: string;
32
+ live: any;
33
+ } | {
34
+ field: string;
35
+ declared: number;
36
+ live: any;
37
+ })[];
29
38
  private discoverCluster;
30
39
  deploy(): Promise<{
31
40
  name: string;
@@ -58,6 +58,25 @@ export class DatabaseBuilder extends BaseBuilder {
58
58
  this._firewallRules.push({ type: "tag", value: tagName });
59
59
  return this;
60
60
  }
61
+ getDiff(existing) {
62
+ const diffs = [];
63
+ if (existing.engine !== this._engine) {
64
+ diffs.push({ field: "engine", declared: this._engine, live: existing.engine });
65
+ }
66
+ if (existing.version !== this._version) {
67
+ diffs.push({ field: "version", declared: this._version, live: existing.version });
68
+ }
69
+ if (existing.size !== this._size) {
70
+ diffs.push({ field: "size", declared: this._size, live: existing.size });
71
+ }
72
+ if (existing.region !== this._region) {
73
+ diffs.push({ field: "region", declared: this._region, live: existing.region });
74
+ }
75
+ if (existing.num_nodes !== this._nodes) {
76
+ diffs.push({ field: "nodes", declared: this._nodes, live: existing.num_nodes });
77
+ }
78
+ return diffs;
79
+ }
61
80
  async discoverCluster(name) {
62
81
  try {
63
82
  const api = getDoApi();
@@ -104,7 +104,7 @@ export class DomainBuilder extends BaseBuilder {
104
104
  if (existing) {
105
105
  try {
106
106
  const res = await api.get(`/domains/${this.domainName}/records?per_page=200`);
107
- existingRecords = res.domain_records;
107
+ existingRecords = res.domain_records ?? [];
108
108
  }
109
109
  catch {
110
110
  existingRecords = [];
@@ -30,6 +30,11 @@ export declare class DropletBuilder extends BaseBuilder {
30
30
  vpc(uuid: string | Output<string>): this;
31
31
  provision(...playbookPaths: (string | string[])[]): this;
32
32
  forceConfigCheck(): this;
33
+ getDiff(existing: any): {
34
+ field: string;
35
+ declared: any;
36
+ live: any;
37
+ }[];
33
38
  protected checkPort(ip: string, port: number): Promise<boolean>;
34
39
  protected runProvisioner(ip: string, script: string): Promise<void>;
35
40
  private resolveOrRegisterSshKey;
@@ -101,6 +101,16 @@ export class DropletBuilder extends BaseBuilder {
101
101
  this._forceConfigCheck = true;
102
102
  return this;
103
103
  }
104
+ getDiff(existing) {
105
+ const diffs = [];
106
+ if (existing.size_slug !== this.config.size) {
107
+ diffs.push({ field: "size", declared: this.config.size, live: existing.size_slug });
108
+ }
109
+ if (existing.region?.slug !== this.config.region) {
110
+ diffs.push({ field: "region", declared: this.config.region, live: existing.region?.slug });
111
+ }
112
+ return diffs;
113
+ }
104
114
  async checkPort(ip, port) {
105
115
  return checkPort(ip, port);
106
116
  }
@@ -20,11 +20,14 @@ function priceForSlug(slug) {
20
20
  }
21
21
  export async function listDoResources() {
22
22
  const api = getDoApi();
23
- const [dropletsData, firewallsData, lbData, domainsData] = await Promise.all([
23
+ const [dropletsData, firewallsData, lbData, domainsData, dbData, appsData, vpcsData] = await Promise.all([
24
24
  api.get('/droplets?per_page=200'),
25
25
  api.get('/firewalls?per_page=200'),
26
26
  api.get('/load_balancers?per_page=200'),
27
27
  api.get('/domains?per_page=200'),
28
+ api.get('/databases?per_page=200'),
29
+ api.get('/apps?per_page=200'),
30
+ api.get('/vpcs?per_page=200'),
28
31
  ]);
29
32
  const droplets = dropletsData.droplets.map((d) => {
30
33
  const pub = (d.networks?.v4 ?? []).find((n) => n.type === 'public');
@@ -54,6 +57,26 @@ export async function listDoResources() {
54
57
  name: d.name,
55
58
  ttl: d.ttl,
56
59
  }));
60
+ const databases = (dbData.databases ?? []).map((d) => ({
61
+ id: d.id,
62
+ name: d.name,
63
+ engine: `${d.engine} ${d.version ?? ''}`.trim(),
64
+ region: d.region ?? '',
65
+ status: d.status ?? '',
66
+ nodeCount: d.num_nodes ?? 1,
67
+ }));
68
+ const apps = (appsData.apps ?? []).map((a) => ({
69
+ id: a.id,
70
+ name: a.spec?.name ?? a.id,
71
+ liveUrl: a.live_url ?? '',
72
+ status: a.active_deployment?.phase ?? 'unknown',
73
+ }));
74
+ const vpcs = (vpcsData.vpcs ?? []).map((v) => ({
75
+ id: v.id,
76
+ name: v.name,
77
+ region: v.region ?? '',
78
+ ipRange: v.ip_range ?? '',
79
+ }));
57
80
  const totalMonthlyCost = droplets.reduce((sum, d) => sum + d.monthlyCost, 0);
58
- return { droplets, firewalls, loadBalancers, domains, totalMonthlyCost };
81
+ return { droplets, firewalls, loadBalancers, domains, databases, apps, vpcs, totalMonthlyCost };
59
82
  }
@@ -41,6 +41,11 @@ export declare class LoadBalancerBuilder extends BaseBuilder {
41
41
  stickySession(type: 'cookies' | 'none', cookieName?: string, cookieTtlSeconds?: number): this;
42
42
  private resolveDropletIds;
43
43
  private resolveCertificateId;
44
+ getDiff(existing: any): {
45
+ field: string;
46
+ declared: string;
47
+ live: any;
48
+ }[];
44
49
  deploy(): Promise<any>;
45
50
  destroy(): Promise<any>;
46
51
  }
@@ -96,6 +96,13 @@ export class LoadBalancerBuilder extends BaseBuilder {
96
96
  }
97
97
  return match.id;
98
98
  }
99
+ getDiff(existing) {
100
+ const diffs = [];
101
+ if (existing.region?.slug !== this._region) {
102
+ diffs.push({ field: "region", declared: this._region, live: existing.region?.slug });
103
+ }
104
+ return diffs;
105
+ }
99
106
  async deploy() {
100
107
  const dryRun = this.isDryRunActive();
101
108
  const existing = await this.discoveryPromise;
@@ -12,6 +12,11 @@ export declare class VPCBuilder extends BaseBuilder {
12
12
  region(r: string): this;
13
13
  ipRange(cidr: string): this;
14
14
  description(text: string): this;
15
+ getDiff(existing: any): {
16
+ field: string;
17
+ declared: string;
18
+ live: any;
19
+ }[];
15
20
  private discoverVpc;
16
21
  deploy(): Promise<{
17
22
  name: string;
@@ -26,6 +26,14 @@ export class VPCBuilder extends BaseBuilder {
26
26
  this._description = text;
27
27
  return this;
28
28
  }
29
+ getDiff(existing) {
30
+ const diffs = [];
31
+ // region and ip_range are immutable after creation
32
+ if (this._description !== undefined && existing.description !== this._description) {
33
+ diffs.push({ field: "description", declared: this._description, live: existing.description });
34
+ }
35
+ return diffs;
36
+ }
29
37
  async discoverVpc(name) {
30
38
  try {
31
39
  const api = getDoApi();
@@ -29,6 +29,15 @@ export declare class FirebaseFunctionsBuilder extends BaseBuilder {
29
29
  maxInstances(n: number): this;
30
30
  minInstances(n: number): this;
31
31
  env(vars: Record<string, string>): this;
32
+ getDiff(existing: any): ({
33
+ field: string;
34
+ declared: string;
35
+ live: any;
36
+ } | {
37
+ field: string;
38
+ declared: number;
39
+ live: any;
40
+ })[];
32
41
  private fnPath;
33
42
  private getExisting;
34
43
  private zipSource;
@@ -79,6 +79,34 @@ export class FirebaseFunctionsBuilder extends BaseBuilder {
79
79
  this._env = vars;
80
80
  return this;
81
81
  }
82
+ getDiff(existing) {
83
+ const diffs = [];
84
+ const liveRuntime = existing.buildConfig?.runtime;
85
+ if (liveRuntime !== undefined && liveRuntime !== this._runtime) {
86
+ diffs.push({ field: "runtime", declared: this._runtime, live: liveRuntime });
87
+ }
88
+ const liveEntryPoint = existing.buildConfig?.entryPoint;
89
+ if (liveEntryPoint !== undefined && liveEntryPoint !== this._entryPoint) {
90
+ diffs.push({ field: "entryPoint", declared: this._entryPoint, live: liveEntryPoint });
91
+ }
92
+ const liveMemory = existing.serviceConfig?.availableMemory;
93
+ if (liveMemory !== undefined && liveMemory !== this._memory) {
94
+ diffs.push({ field: "memory", declared: this._memory, live: liveMemory });
95
+ }
96
+ const liveTimeout = existing.serviceConfig?.timeoutSeconds;
97
+ if (liveTimeout !== undefined && liveTimeout !== this._timeout) {
98
+ diffs.push({ field: "timeout", declared: `${this._timeout}s`, live: `${liveTimeout}s` });
99
+ }
100
+ const liveMax = existing.serviceConfig?.maxInstanceCount;
101
+ if (liveMax !== undefined && liveMax !== this._maxInstances) {
102
+ diffs.push({ field: "maxInstances", declared: this._maxInstances, live: liveMax });
103
+ }
104
+ const liveMin = existing.serviceConfig?.minInstanceCount ?? 0;
105
+ if (liveMin !== this._minInstances) {
106
+ diffs.push({ field: "minInstances", declared: this._minInstances, live: liveMin });
107
+ }
108
+ return diffs;
109
+ }
82
110
  fnPath() {
83
111
  return `/projects/${getProjectId()}/locations/${this._region}/functions/${this.name}`;
84
112
  }
@@ -1,9 +1,13 @@
1
1
  import { getProjectId, hostingFetch, cloudFetch } from "./api.js";
2
2
  export async function listFirebaseResources() {
3
3
  const project = getProjectId();
4
- const [hostRes, fnRes] = await Promise.all([
4
+ const [hostRes, fnRes, firestoreRes, storageRes, authRes, rcRes] = await Promise.all([
5
5
  hostingFetch(`/projects/${project}/sites`).catch(() => ({})),
6
6
  cloudFetch("https://cloudfunctions.googleapis.com/v2", `/projects/${project}/locations/-/functions`).catch(() => ({})),
7
+ cloudFetch("https://firestore.googleapis.com/v1", `/projects/${project}/databases`).catch(() => ({})),
8
+ cloudFetch("https://storage.googleapis.com/storage/v1", `/b?project=${project}`).catch(() => ({})),
9
+ cloudFetch("https://identitytoolkit.googleapis.com/admin/v2", `/projects/${project}/config`).catch(() => ({})),
10
+ cloudFetch("https://firebaseremoteconfig.googleapis.com/v1", `/projects/${project}/remoteConfig`).catch(() => ({})),
7
11
  ]);
8
12
  // 1. Map Hosting Sites
9
13
  const hostingSites = (hostRes.sites ?? []).map((s) => ({
@@ -21,5 +25,33 @@ export async function listFirebaseResources() {
21
25
  runtime: f.buildConfig?.runtime ?? "unknown",
22
26
  };
23
27
  });
24
- return { hostingSites, functions };
28
+ // 3. Map Firestore Databases
29
+ const firestoreDbs = (firestoreRes.databases ?? []).map((d) => ({
30
+ name: d.name.split("/").pop() ?? d.name,
31
+ type: d.type ?? "FIRESTORE_NATIVE",
32
+ state: d.state ?? "unknown",
33
+ }));
34
+ // 4. Map Storage Buckets (filter to project-owned buckets)
35
+ const storageBuckets = (storageRes.items ?? [])
36
+ .filter((b) => b.name?.includes(project))
37
+ .map((b) => ({
38
+ name: b.name,
39
+ location: b.location ?? "unknown",
40
+ }));
41
+ // 5. Map Auth Sign-in Providers from Identity Toolkit config
42
+ const signIn = authRes.signIn ?? {};
43
+ const authProviders = [
44
+ ...(signIn.email?.enabled ? [{ providerId: "email/password" }] : []),
45
+ ...(signIn.phoneNumber?.enabled ? [{ providerId: "phone" }] : []),
46
+ ...(signIn.anonymous?.enabled ? [{ providerId: "anonymous" }] : []),
47
+ ];
48
+ // 6. Map RemoteConfig
49
+ let remoteConfig;
50
+ if (rcRes.parameters !== undefined || rcRes.version) {
51
+ remoteConfig = {
52
+ parameterCount: Object.keys(rcRes.parameters ?? {}).length,
53
+ version: rcRes.version?.versionNumber ?? "unknown",
54
+ };
55
+ }
56
+ return { hostingSites, functions, firestoreDbs, storageBuckets, authProviders, remoteConfig };
25
57
  }
@@ -113,6 +113,12 @@ function createGcpOfflineMock(base, path, opts) {
113
113
  };
114
114
  }
115
115
  if (path.includes("/instances")) {
116
+ // Specific instance GET (discovery path: /instances/{name}) - return not-found so
117
+ // builders plan creation rather than treating the mock VM as already existing
118
+ const afterInstances = path.split("/instances")[1] ?? "";
119
+ if (afterInstances.startsWith("/") && afterInstances.length > 1) {
120
+ throw new Error(`GCP API GET ${path} → 404: Not Found`);
121
+ }
116
122
  return {
117
123
  status: "RUNNING",
118
124
  id: "mock-gcp-instance-id",
@@ -20,6 +20,19 @@ export declare class GCPCloudRunBuilder extends BaseBuilder {
20
20
  env(vars: Record<string, string | GCPSecretBuilder>): this;
21
21
  region(reg: string): this;
22
22
  public(enabled?: boolean): this;
23
+ getDiff(existing: any): ({
24
+ field: string;
25
+ declared: string | undefined;
26
+ live: any;
27
+ } | {
28
+ field: string;
29
+ declared: number;
30
+ live: any;
31
+ } | {
32
+ field: string;
33
+ declared: boolean;
34
+ live: boolean;
35
+ })[];
23
36
  private discoverService;
24
37
  deploy(): Promise<{
25
38
  serviceId: string;
@@ -60,6 +60,36 @@ export class GCPCloudRunBuilder extends BaseBuilder {
60
60
  this._public = enabled;
61
61
  return this;
62
62
  }
63
+ getDiff(existing) {
64
+ const diffs = [];
65
+ const container = existing.template?.containers?.[0];
66
+ const scaling = existing.template?.scaling ?? {};
67
+ const targetIngress = this._public ? "INGRESS_TRAFFIC_ALL" : "INGRESS_TRAFFIC_INTERNAL_ONLY";
68
+ if (container?.image !== this._image) {
69
+ diffs.push({ field: "image", declared: this._image, live: container?.image });
70
+ }
71
+ if (container?.ports?.[0]?.containerPort !== this._port) {
72
+ diffs.push({ field: "port", declared: this._port, live: container?.ports?.[0]?.containerPort });
73
+ }
74
+ if (container?.resources?.limits?.cpu !== formatCpu(this._cpu)) {
75
+ diffs.push({ field: "cpu", declared: formatCpu(this._cpu), live: container?.resources?.limits?.cpu });
76
+ }
77
+ if (container?.resources?.limits?.memory !== formatMemory(this._memory)) {
78
+ diffs.push({ field: "memory", declared: formatMemory(this._memory), live: container?.resources?.limits?.memory });
79
+ }
80
+ const minDeclared = this._minInstances ?? 0;
81
+ const maxDeclared = this._maxInstances ?? 100;
82
+ if ((scaling.minInstanceCount ?? 0) !== minDeclared) {
83
+ diffs.push({ field: "minInstances", declared: minDeclared, live: scaling.minInstanceCount ?? 0 });
84
+ }
85
+ if ((scaling.maxInstanceCount ?? 100) !== maxDeclared) {
86
+ diffs.push({ field: "maxInstances", declared: maxDeclared, live: scaling.maxInstanceCount ?? 100 });
87
+ }
88
+ if (existing.ingress !== targetIngress) {
89
+ diffs.push({ field: "public", declared: this._public, live: existing.ingress === "INGRESS_TRAFFIC_ALL" });
90
+ }
91
+ return diffs;
92
+ }
63
93
  async discoverService() {
64
94
  try {
65
95
  const project = getProjectId();
@@ -23,6 +23,15 @@ export declare class GCPCloudSQLBuilder extends BaseBuilder {
23
23
  database(name: string): this;
24
24
  publicAccess(enabled?: boolean): this;
25
25
  region(reg: string): this;
26
+ getDiff(existing: any): ({
27
+ field: string;
28
+ declared: string;
29
+ live: any;
30
+ } | {
31
+ field: string;
32
+ declared: boolean;
33
+ live: boolean;
34
+ })[];
26
35
  private discoverInstance;
27
36
  private waitForOperation;
28
37
  deploy(): Promise<{
@@ -64,6 +64,26 @@ export class GCPCloudSQLBuilder extends BaseBuilder {
64
64
  this.discoveryPromise = this.discoverInstance();
65
65
  return this;
66
66
  }
67
+ getDiff(existing) {
68
+ const diffs = [];
69
+ const declaredDbVersion = `${this._engine.toUpperCase()}_${this._engineVersion}`;
70
+ if (existing.databaseVersion !== declaredDbVersion) {
71
+ diffs.push({ field: "databaseVersion", declared: declaredDbVersion, live: existing.databaseVersion });
72
+ }
73
+ const liveTier = existing.settings?.tier;
74
+ if (liveTier !== undefined && liveTier !== this._tier) {
75
+ diffs.push({ field: "tier", declared: this._tier, live: liveTier });
76
+ }
77
+ const liveDiskSize = existing.settings?.dataDiskSizeGb;
78
+ if (liveDiskSize !== undefined && Number(liveDiskSize) !== this._storage) {
79
+ diffs.push({ field: "storage", declared: `${this._storage} GB`, live: `${liveDiskSize} GB` });
80
+ }
81
+ const isPublic = (existing.settings?.ipConfiguration?.authorizedNetworks ?? []).length > 0;
82
+ if (isPublic !== this._publicAccess) {
83
+ diffs.push({ field: "publicAccess", declared: this._publicAccess, live: isPublic });
84
+ }
85
+ return diffs;
86
+ }
67
87
  async discoverInstance() {
68
88
  try {
69
89
  const project = getProjectId();
@@ -1,11 +1,13 @@
1
1
  import { gcpFetch, getProjectId } from "./api.js";
2
2
  export async function listGcpResources() {
3
3
  const project = getProjectId();
4
- const [vmRes, sqlRes, runRes, dnsRes] = await Promise.all([
4
+ const [vmRes, sqlRes, runRes, dnsRes, pubsubRes, secretsRes] = await Promise.all([
5
5
  gcpFetch("https://compute.googleapis.com", `/compute/v1/projects/${project}/aggregated/instances`).catch(() => ({})),
6
6
  gcpFetch("https://sqladmin.googleapis.com", `/v1/projects/${project}/instances`).catch(() => ({})),
7
7
  gcpFetch("https://run.googleapis.com", `/v2/projects/${project}/locations/-/services`).catch(() => ({})),
8
8
  gcpFetch("https://dns.googleapis.com", `/dns/v1/projects/${project}/managedZones`).catch(() => ({})),
9
+ gcpFetch("https://pubsub.googleapis.com", `/v1/projects/${project}/topics`).catch(() => ({})),
10
+ gcpFetch("https://secretmanager.googleapis.com", `/v1/projects/${project}/secrets`).catch(() => ({})),
9
11
  ]);
10
12
  // 1. Map VM Instances
11
13
  const vms = [];
@@ -51,5 +53,13 @@ export async function listGcpResources() {
51
53
  name: z.name,
52
54
  dnsName: z.dnsName ?? "",
53
55
  }));
54
- return { vms, rdsInstances, distributions, hostedZones };
56
+ // 5. Map Pub/Sub Topics
57
+ const pubSubTopics = (pubsubRes.topics ?? []).map((t) => ({
58
+ name: t.name.split("/").pop() ?? t.name,
59
+ }));
60
+ // 6. Map Secret Manager Secrets
61
+ const secrets = (secretsRes.secrets ?? []).map((s) => ({
62
+ name: s.name.split("/").pop() ?? s.name,
63
+ }));
64
+ return { vms, rdsInstances, distributions, hostedZones, pubSubTopics, secrets };
55
65
  }
@@ -28,6 +28,11 @@ export declare class GCPVMBuilder extends BaseBuilder {
28
28
  private resolveUser;
29
29
  provision(...playbookPaths: (string | string[])[]): this;
30
30
  forceConfigCheck(): this;
31
+ getDiff(existing: any): {
32
+ field: string;
33
+ declared: string;
34
+ live: any;
35
+ }[];
31
36
  protected checkPort(ip: string, port: number): Promise<boolean>;
32
37
  protected runProvisioner(ip: string, script: string): Promise<void>;
33
38
  private discoverVM;
@@ -71,6 +71,14 @@ export class GCPVMBuilder extends BaseBuilder {
71
71
  this._forceConfigCheck = true;
72
72
  return this;
73
73
  }
74
+ getDiff(existing) {
75
+ const diffs = [];
76
+ const liveMachineType = existing.machineType?.split("/").pop();
77
+ if (liveMachineType !== undefined && liveMachineType !== this._machineType) {
78
+ diffs.push({ field: "machineType", declared: this._machineType, live: liveMachineType });
79
+ }
80
+ return diffs;
81
+ }
74
82
  async checkPort(ip, port) {
75
83
  return checkPort(ip, port);
76
84
  }
@@ -11,5 +11,12 @@ export async function listProxmoxVMs() {
11
11
  maxmem: r.maxmem ?? 0,
12
12
  maxdisk: r.maxdisk ?? 0,
13
13
  }));
14
- return { vms };
14
+ const templates = (resources ?? [])
15
+ .filter((r) => r.template === 1)
16
+ .map((r) => ({
17
+ name: r.name,
18
+ vmid: r.vmid,
19
+ node: r.node,
20
+ }));
21
+ return { vms, templates };
15
22
  }
@@ -38,6 +38,19 @@ export declare class VMBuilder extends ProxmoxBaseBuilder {
38
38
  gateway(gw: string): this;
39
39
  machine(type: "q35" | "i440fx"): this;
40
40
  forceConfigCheck(): this;
41
+ getDiff(existing: any): ({
42
+ field: string;
43
+ declared: number;
44
+ live: any;
45
+ } | {
46
+ field: string;
47
+ declared: string;
48
+ live: string;
49
+ } | {
50
+ field: string;
51
+ declared: "q35" | "i440fx";
52
+ live: any;
53
+ })[];
41
54
  deploy(): Promise<{
42
55
  name: string;
43
56
  vmid: number | null;