puls-dev 0.2.9 → 0.3.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.
@@ -1,3 +1,4 @@
1
+ import "dotenv/config";
1
2
  export interface GlobalConfig {
2
3
  dryRun?: boolean;
3
4
  parallel?: boolean;
@@ -1,3 +1,4 @@
1
+ import "dotenv/config";
1
2
  class ConfigManager {
2
3
  config = {
3
4
  providers: {},
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,94 @@
1
+ import { test, describe } from "node:test";
2
+ import assert from "node:assert";
3
+ import { BaseBuilder, createBuilderArray } from "./resource.js";
4
+ import { Stack } from "./stack.js";
5
+ import { Config } from "./config.js";
6
+ class MockResourceBuilder extends BaseBuilder {
7
+ val = 0;
8
+ strVal = "";
9
+ deployCount = 0;
10
+ destroyCount = 0;
11
+ constructor(name) {
12
+ super(name);
13
+ }
14
+ setVal(n) {
15
+ // Overloaded to support array assignment in proxy test
16
+ if (typeof n === "number") {
17
+ this.val = n;
18
+ }
19
+ return this;
20
+ }
21
+ setStr(s) {
22
+ if (typeof s === "string") {
23
+ this.strVal = s;
24
+ }
25
+ return this;
26
+ }
27
+ async deploy() {
28
+ this.deployCount++;
29
+ return { name: this.name, val: this.val, strVal: this.strVal };
30
+ }
31
+ async destroy() {
32
+ this.destroyCount++;
33
+ return { destroyedName: this.name };
34
+ }
35
+ }
36
+ describe("Resource Builder Groups Unit Tests", () => {
37
+ test("createBuilderArray proxies and chains methods", () => {
38
+ const b1 = new MockResourceBuilder("r1");
39
+ const b2 = new MockResourceBuilder("r2");
40
+ const group = createBuilderArray([b1, b2]);
41
+ // Test method chaining and uniform distribution
42
+ group.setVal(42).setStr("hello");
43
+ assert.strictEqual(b1.val, 42);
44
+ assert.strictEqual(b2.val, 42);
45
+ assert.strictEqual(b1.strVal, "hello");
46
+ assert.strictEqual(b2.strVal, "hello");
47
+ });
48
+ test("createBuilderArray distributes array arguments by index", () => {
49
+ const b1 = new MockResourceBuilder("r1");
50
+ const b2 = new MockResourceBuilder("r2");
51
+ const group = createBuilderArray([b1, b2]);
52
+ // Test index-based distribution
53
+ group.setVal([10, 20]).setStr(["first", "second"]);
54
+ // In MockResourceBuilder, calling setVal with [10, 20] gets distributed by proxy:
55
+ // b1 gets setVal(10) -> val = 10
56
+ // b2 gets setVal(20) -> val = 20
57
+ assert.strictEqual(b1.val, 10);
58
+ assert.strictEqual(b2.val, 20);
59
+ assert.strictEqual(b1.strVal, "first");
60
+ assert.strictEqual(b2.strVal, "second");
61
+ });
62
+ test("Stack deploy gathers array builders and structures outputs as array", async () => {
63
+ Config.set({ dryRun: false });
64
+ const b1 = new MockResourceBuilder("s1");
65
+ const b2 = new MockResourceBuilder("s2");
66
+ class ArrayStack extends Stack {
67
+ servers = createBuilderArray([b1, b2]).setVal([100, 200]);
68
+ }
69
+ const stack = new ArrayStack();
70
+ const result = await stack.deploy();
71
+ assert.strictEqual(b1.deployCount, 1);
72
+ assert.strictEqual(b2.deployCount, 1);
73
+ assert.ok(Array.isArray(result.servers));
74
+ assert.strictEqual(result.servers.length, 2);
75
+ assert.deepStrictEqual(result.servers[0], { name: "s1", val: 100, strVal: "" });
76
+ assert.deepStrictEqual(result.servers[1], { name: "s2", val: 200, strVal: "" });
77
+ });
78
+ test("Stack destroy gathers array builders and structures outputs as array", async () => {
79
+ Config.set({ dryRun: false });
80
+ const b1 = new MockResourceBuilder("s1");
81
+ const b2 = new MockResourceBuilder("s2");
82
+ class ArrayStack extends Stack {
83
+ servers = createBuilderArray([b1, b2]);
84
+ }
85
+ const stack = new ArrayStack();
86
+ const result = await stack.destroy();
87
+ assert.strictEqual(b1.destroyCount, 1);
88
+ assert.strictEqual(b2.destroyCount, 1);
89
+ assert.ok(Array.isArray(result.servers));
90
+ assert.strictEqual(result.servers.length, 2);
91
+ assert.deepStrictEqual(result.servers[0], { destroyedName: "s1" });
92
+ assert.deepStrictEqual(result.servers[1], { destroyedName: "s2" });
93
+ });
94
+ });
@@ -41,3 +41,4 @@ export declare abstract class BaseBuilder {
41
41
  destroy(): Promise<any>;
42
42
  abstract deploy(): Promise<any>;
43
43
  }
44
+ export declare function createBuilderArray<T extends BaseBuilder>(builders: T[]): T[] & T;
@@ -129,3 +129,38 @@ export class BaseBuilder {
129
129
  return { destroyed: this.name };
130
130
  }
131
131
  }
132
+ export function createBuilderArray(builders) {
133
+ return new Proxy(builders, {
134
+ get(target, prop, receiver) {
135
+ if (prop in target) {
136
+ const val = Reflect.get(target, prop, receiver);
137
+ if (typeof val === "function") {
138
+ return val.bind(target);
139
+ }
140
+ return val;
141
+ }
142
+ if (builders.length > 0) {
143
+ const first = builders[0];
144
+ const val = Reflect.get(first, prop);
145
+ if (typeof val === "function") {
146
+ return function (...args) {
147
+ builders.forEach((b, idx) => {
148
+ const method = Reflect.get(b, prop);
149
+ if (typeof method === "function") {
150
+ const mappedArgs = args.map((arg) => {
151
+ if (Array.isArray(arg) && arg.length === builders.length) {
152
+ return arg[idx];
153
+ }
154
+ return arg;
155
+ });
156
+ method.apply(b, mappedArgs);
157
+ }
158
+ });
159
+ return receiver;
160
+ };
161
+ }
162
+ }
163
+ return undefined;
164
+ },
165
+ });
166
+ }
@@ -170,6 +170,20 @@ export class Stack {
170
170
  val.forceConfigCheck();
171
171
  }
172
172
  }
173
+ else if (Array.isArray(val)) {
174
+ const isProtected = Reflect.getMetadata("protected", this, prop);
175
+ const forceConfigCheck = Reflect.getMetadata("forceConfigCheck", this, prop);
176
+ for (const item of val) {
177
+ if (item instanceof BaseBuilder) {
178
+ resources.push({ prop, resource: item });
179
+ if (isProtected)
180
+ item.protect();
181
+ if (forceConfigCheck && typeof item.forceConfigCheck === "function") {
182
+ item.forceConfigCheck();
183
+ }
184
+ }
185
+ }
186
+ }
173
187
  }
174
188
  // 2. Schedule execution
175
189
  if (isParallel) {
@@ -203,7 +217,19 @@ export class Stack {
203
217
  res = await resource.deploy();
204
218
  await resource._runAfterDeploy(res);
205
219
  }
206
- outputs[prop] = res;
220
+ const propVal = this[prop];
221
+ if (Array.isArray(propVal)) {
222
+ const idx = propVal.indexOf(resource);
223
+ if (idx !== -1) {
224
+ if (!outputs[prop]) {
225
+ outputs[prop] = [];
226
+ }
227
+ outputs[prop][idx] = res;
228
+ }
229
+ }
230
+ else {
231
+ outputs[prop] = res;
232
+ }
207
233
  return res;
208
234
  }
209
235
  catch (err) {
@@ -234,7 +260,19 @@ export class Stack {
234
260
  res = await resource.deploy();
235
261
  await resource._runAfterDeploy(res);
236
262
  }
237
- outputs[prop] = res;
263
+ const propVal = this[prop];
264
+ if (Array.isArray(propVal)) {
265
+ const idx = propVal.indexOf(resource);
266
+ if (idx !== -1) {
267
+ if (!outputs[prop]) {
268
+ outputs[prop] = [];
269
+ }
270
+ outputs[prop][idx] = res;
271
+ }
272
+ }
273
+ else {
274
+ outputs[prop] = res;
275
+ }
238
276
  }
239
277
  catch (err) {
240
278
  controller.abort();
@@ -322,6 +360,17 @@ export class Stack {
322
360
  }
323
361
  resources.push({ prop, resource: val });
324
362
  }
363
+ else if (Array.isArray(val)) {
364
+ if (Reflect.getMetadata("protected", this, prop)) {
365
+ console.log(` 🔒 Skipping protected resource "${prop}"`);
366
+ continue;
367
+ }
368
+ for (const item of val) {
369
+ if (item instanceof BaseBuilder) {
370
+ resources.push({ prop, resource: item });
371
+ }
372
+ }
373
+ }
325
374
  }
326
375
  // 2. Schedule execution
327
376
  if (isParallel) {
@@ -348,7 +397,19 @@ export class Stack {
348
397
  await resource._runBeforeDestroy();
349
398
  const res = await resource.destroy();
350
399
  await resource._runAfterDestroy(res);
351
- outputs[prop] = res;
400
+ const propVal = this[prop];
401
+ if (Array.isArray(propVal)) {
402
+ const idx = propVal.indexOf(resource);
403
+ if (idx !== -1) {
404
+ if (!outputs[prop]) {
405
+ outputs[prop] = [];
406
+ }
407
+ outputs[prop][idx] = res;
408
+ }
409
+ }
410
+ else {
411
+ outputs[prop] = res;
412
+ }
352
413
  return res;
353
414
  }
354
415
  catch (err) {
@@ -370,7 +431,19 @@ export class Stack {
370
431
  await resource._runBeforeDestroy();
371
432
  const res = await resource.destroy();
372
433
  await resource._runAfterDestroy(res);
373
- outputs[prop] = res;
434
+ const propVal = this[prop];
435
+ if (Array.isArray(propVal)) {
436
+ const idx = propVal.indexOf(resource);
437
+ if (idx !== -1) {
438
+ if (!outputs[prop]) {
439
+ outputs[prop] = [];
440
+ }
441
+ outputs[prop][idx] = res;
442
+ }
443
+ }
444
+ else {
445
+ outputs[prop] = res;
446
+ }
374
447
  }
375
448
  catch (err) {
376
449
  controller.abort();
@@ -1,6 +1,7 @@
1
1
  import { GetSecretValueCommand, CreateSecretCommand, PutSecretValueCommand, DeleteSecretCommand, } from "@aws-sdk/client-secrets-manager";
2
2
  import { BaseBuilder } from "../../core/resource.js";
3
3
  import { getSecretsClient } from "./api.js";
4
+ import { resolvedSecrets } from "../../core/secret.js";
4
5
  export class SecretsBuilder extends BaseBuilder {
5
6
  _value;
6
7
  _description;
@@ -16,6 +17,9 @@ export class SecretsBuilder extends BaseBuilder {
16
17
  try {
17
18
  const result = await getSecretsClient().send(new GetSecretValueCommand({ SecretId: secretId }));
18
19
  this.resolvedValue = result.SecretString ?? null;
20
+ if (this.resolvedValue && this.resolvedValue.length >= 3) {
21
+ resolvedSecrets.add(this.resolvedValue);
22
+ }
19
23
  this.resolvedArn = result.ARN ?? null;
20
24
  return result;
21
25
  }
@@ -38,10 +42,17 @@ export class SecretsBuilder extends BaseBuilder {
38
42
  }
39
43
  plainText(v) {
40
44
  this._value = v;
45
+ if (v && v.length >= 3) {
46
+ resolvedSecrets.add(v);
47
+ }
41
48
  return this;
42
49
  }
43
50
  keyValue(obj) {
44
- this._value = JSON.stringify(obj);
51
+ const v = JSON.stringify(obj);
52
+ this._value = v;
53
+ if (v && v.length >= 3) {
54
+ resolvedSecrets.add(v);
55
+ }
45
56
  return this;
46
57
  }
47
58
  description(d) {
@@ -61,7 +72,7 @@ export class SecretsBuilder extends BaseBuilder {
61
72
  if (existing) {
62
73
  console.log(` ✅ Secret "${this.name}" exists`);
63
74
  if (this.resolvedValue !== null)
64
- console.log(` 💬 Value: ${this.resolvedValue}`);
75
+ console.log(` 💬 Value: ********`);
65
76
  if (this._value)
66
77
  console.log(` 📝 [PLAN] Update secret value`);
67
78
  }
@@ -88,18 +99,24 @@ export class SecretsBuilder extends BaseBuilder {
88
99
  }));
89
100
  this.resolvedArn = result.ARN ?? null;
90
101
  this.resolvedValue = this._value;
102
+ if (this._value && this._value.length >= 3) {
103
+ resolvedSecrets.add(this._value);
104
+ }
91
105
  console.log(`🚀 Created secret "${this.name}"`);
92
106
  }
93
107
  else {
94
108
  console.log(` ✅ Secret "${this.name}" exists`);
95
109
  if (this.resolvedValue !== null)
96
- console.log(` 💬 Value: ${this.resolvedValue}`);
110
+ console.log(` 💬 Value: ********`);
97
111
  if (this._value && this._value !== this.resolvedValue) {
98
112
  await client.send(new PutSecretValueCommand({
99
113
  SecretId: this.name,
100
114
  SecretString: this._value,
101
115
  }));
102
116
  this.resolvedValue = this._value;
117
+ if (this._value && this._value.length >= 3) {
118
+ resolvedSecrets.add(this._value);
119
+ }
103
120
  console.log(` ✅ Updated secret value`);
104
121
  }
105
122
  }
@@ -1,5 +1,6 @@
1
1
  import { BaseBuilder } from "../../core/resource.js";
2
2
  import { gcpFetch, getProjectId } from "./api.js";
3
+ import { resolvedSecrets } from "../../core/secret.js";
3
4
  const SECRET_BASE = "https://secretmanager.googleapis.com";
4
5
  export class GCPSecretBuilder extends BaseBuilder {
5
6
  _value;
@@ -20,6 +21,9 @@ export class GCPSecretBuilder extends BaseBuilder {
20
21
  const payload = await gcpFetch(SECRET_BASE, `/v1/projects/${project}/secrets/${secretId}/versions/latest:access`);
21
22
  if (payload.payload?.data) {
22
23
  this.resolvedValue = Buffer.from(payload.payload.data, "base64").toString("utf8");
24
+ if (this.resolvedValue && this.resolvedValue.length >= 3) {
25
+ resolvedSecrets.add(this.resolvedValue);
26
+ }
23
27
  }
24
28
  }
25
29
  catch (err) {
@@ -42,10 +46,17 @@ export class GCPSecretBuilder extends BaseBuilder {
42
46
  }
43
47
  plainText(v) {
44
48
  this._value = v;
49
+ if (v && v.length >= 3) {
50
+ resolvedSecrets.add(v);
51
+ }
45
52
  return this;
46
53
  }
47
54
  keyValue(obj) {
48
- this._value = JSON.stringify(obj);
55
+ const v = JSON.stringify(obj);
56
+ this._value = v;
57
+ if (v && v.length >= 3) {
58
+ resolvedSecrets.add(v);
59
+ }
49
60
  return this;
50
61
  }
51
62
  async deploy() {
@@ -58,7 +69,7 @@ export class GCPSecretBuilder extends BaseBuilder {
58
69
  if (existing) {
59
70
  console.log(` ✅ Secret "${secretId}" exists`);
60
71
  if (this.resolvedValue !== null) {
61
- console.log(` 💬 Value: ${this.resolvedValue}`);
72
+ console.log(` 💬 Value: ********`);
62
73
  }
63
74
  if (this._value) {
64
75
  console.log(` 📝 [PLAN] Update secret value`);
@@ -101,12 +112,15 @@ export class GCPSecretBuilder extends BaseBuilder {
101
112
  }),
102
113
  });
103
114
  this.resolvedValue = this._value;
115
+ if (this._value && this._value.length >= 3) {
116
+ resolvedSecrets.add(this._value);
117
+ }
104
118
  console.log(`🚀 Created secret "${secretId}"`);
105
119
  }
106
120
  else {
107
121
  console.log(` ✅ Secret "${secretId}" exists`);
108
122
  if (this.resolvedValue !== null) {
109
- console.log(` 💬 Value: ${this.resolvedValue}`);
123
+ console.log(` 💬 Value: ********`);
110
124
  }
111
125
  if (this._value && this._value !== this.resolvedValue) {
112
126
  const base64Data = Buffer.from(this._value, "utf8").toString("base64");
@@ -119,6 +133,9 @@ export class GCPSecretBuilder extends BaseBuilder {
119
133
  }),
120
134
  });
121
135
  this.resolvedValue = this._value;
136
+ if (this._value && this._value.length >= 3) {
137
+ resolvedSecrets.add(this._value);
138
+ }
122
139
  console.log(` ✅ Updated secret value`);
123
140
  }
124
141
  }
@@ -12,7 +12,7 @@ export declare const Proxmox: {
12
12
  dnsServers?: string[];
13
13
  verifySsl?: boolean;
14
14
  }) => void;
15
- VM: (name: string) => VMBuilder;
16
- Template: (name: string) => TemplateBuilder;
15
+ VM: <T extends string | string[]>(name: T) => T extends string[] ? VMBuilder[] & VMBuilder : VMBuilder;
16
+ Template: <T extends string | string[]>(name: T) => T extends string[] ? TemplateBuilder[] & TemplateBuilder : TemplateBuilder;
17
17
  };
18
18
  export * from "../../types/proxmox.js";
@@ -1,13 +1,24 @@
1
1
  import { Config } from "../../core/config.js";
2
2
  import { VMBuilder } from "./vm.js";
3
3
  import { TemplateBuilder } from "./template.js";
4
+ import { createBuilderArray } from "../../core/resource.js";
4
5
  export const Proxmox = {
5
6
  init: (opts) => {
6
7
  Config.set({
7
8
  providers: { ...Config.get().providers, proxmox: opts },
8
9
  });
9
10
  },
10
- VM: (name) => new VMBuilder(name),
11
- Template: (name) => new TemplateBuilder(name),
11
+ VM: (name) => {
12
+ if (Array.isArray(name)) {
13
+ return createBuilderArray(name.map((n) => new VMBuilder(n)));
14
+ }
15
+ return new VMBuilder(name);
16
+ },
17
+ Template: (name) => {
18
+ if (Array.isArray(name)) {
19
+ return createBuilderArray(name.map((n) => new TemplateBuilder(n)));
20
+ }
21
+ return new TemplateBuilder(name);
22
+ },
12
23
  };
13
24
  export * from "../../types/proxmox.js";
@@ -166,14 +166,15 @@ export class TemplateBuilder extends BaseBuilder {
166
166
  }
167
167
  if (baseTemplate) {
168
168
  console.log(` 📋 Cloning base template "${baseTemplate.name}" (vmid=${baseTemplate.vmid}) → "${this.name}" (vmid=${newVmid})`);
169
- const taskId = await pm.post(`/nodes/${node}/qemu/${baseTemplate.vmid}/clone`, {
169
+ const taskId = await pm.post(`/nodes/${baseTemplate.node || node}/qemu/${baseTemplate.vmid}/clone`, {
170
170
  newid: newVmid,
171
171
  name: this.name,
172
172
  full: 1,
173
173
  storage,
174
174
  format: "raw",
175
+ target: node,
175
176
  });
176
- await this.waitForTask(node, taskId, pm);
177
+ await this.waitForTask(baseTemplate.node || node, taskId, pm);
177
178
  }
178
179
  else {
179
180
  console.log(` 🆕 Creating blank VM "${this.name}" (vmid=${newVmid})`);
@@ -139,13 +139,13 @@ describe("Proxmox TemplateBuilder Unit Tests", () => {
139
139
  const templateCall = clientCalls.find(c => c.method === "POST" && c.path === "/nodes/pve1/qemu/600/template");
140
140
  assert.ok(templateCall);
141
141
  });
142
- test("VM clones from Template successfully", async () => {
142
+ test("VM clones from Template successfully across nodes", async () => {
143
143
  const nginxHash = getFileHash("playbooks/nginx.yaml");
144
144
  const notes = mergeProvisionMetadata("Pre-baked template notes", {
145
145
  "nginx.yaml": nginxHash,
146
146
  });
147
147
  mockGetResponses["/cluster/resources?type=vm"] = [
148
- // Template exists
148
+ // Template exists on pve1
149
149
  { name: "my-game-template", vmid: 500, node: "pve1", template: 1 },
150
150
  ];
151
151
  mockGetResponses["/nodes/pve1/qemu/500/config"] = {
@@ -153,7 +153,8 @@ describe("Proxmox TemplateBuilder Unit Tests", () => {
153
153
  };
154
154
  mockGetResponses["/cluster/nextid"] = 205;
155
155
  mockGetResponses["/nodes"] = [
156
- { node: "pve1", status: "online", maxmem: 32 * 1024 * 1024 * 1024, mem: 12 * 1024 * 1024 * 1024 }
156
+ // Target node is pve2
157
+ { node: "pve2", status: "online", maxmem: 32 * 1024 * 1024 * 1024, mem: 12 * 1024 * 1024 * 1024 }
157
158
  ];
158
159
  class ProxmoxStack extends Stack {
159
160
  template = new TemplateBuilder("my-game-template")
@@ -170,10 +171,45 @@ describe("Proxmox TemplateBuilder Unit Tests", () => {
170
171
  // Verify VM cloned successfully
171
172
  assert.strictEqual(result.template.vmid, 500);
172
173
  assert.strictEqual(result.server.vmid, 205);
173
- // Verify clone POST used the template's VMID
174
+ // Verify clone POST used the template's node (pve1) and vmid (500), but with target (pve2)
174
175
  const cloneCall = clientCalls.find(c => c.method === "POST" && c.path === "/nodes/pve1/qemu/500/clone");
175
176
  assert.ok(cloneCall);
176
177
  assert.strictEqual(cloneCall.body.newid, 205);
177
178
  assert.strictEqual(cloneCall.body.name, "my-prod-game-01");
179
+ assert.strictEqual(cloneCall.body.target, "pve2");
180
+ });
181
+ test("Template bakes from baseTemplate on a different node successfully", async () => {
182
+ mockGetResponses["/cluster/resources?type=vm"] = [
183
+ // Base template exists on pve1
184
+ { name: "ubuntu-base", vmid: 9000, node: "pve1", template: 1 },
185
+ ];
186
+ mockGetResponses["/nodes"] = [
187
+ // Target node pve2 has more free RAM than pve1
188
+ { node: "pve1", status: "online", maxmem: 32 * 1024 * 1024 * 1024, mem: 20 * 1024 * 1024 * 1024 },
189
+ { node: "pve2", status: "online", maxmem: 32 * 1024 * 1024 * 1024, mem: 5 * 1024 * 1024 * 1024 },
190
+ ];
191
+ mockGetResponses["/cluster/nextid"] = 9001;
192
+ mockGetResponses["/nodes/pve2/qemu/9001/agent/network-get-interfaces"] = [
193
+ {
194
+ name: "eth0",
195
+ "ip-addresses": [
196
+ { "ip-address-type": "ipv4", "ip-address": "10.8.10.199" }
197
+ ]
198
+ }
199
+ ];
200
+ const builder = new TemplateBuilder("my-new-template")
201
+ .baseImage("ubuntu-base")
202
+ .provision("playbooks/nginx.yaml");
203
+ builder.waitFor = async () => true;
204
+ builder.checkPort = async () => true;
205
+ builder.checkCloudInit = async () => true;
206
+ const result = await builder.deploy();
207
+ assert.strictEqual(result.vmid, 9001);
208
+ assert.strictEqual(result.node, "pve2");
209
+ // Verify clone POST was sent to pve1 (source node) but targeted pve2
210
+ const cloneCall = clientCalls.find(c => c.method === "POST" && c.path === "/nodes/pve1/qemu/9000/clone");
211
+ assert.ok(cloneCall);
212
+ assert.strictEqual(cloneCall.body.newid, 9001);
213
+ assert.strictEqual(cloneCall.body.target, "pve2");
178
214
  });
179
215
  });
@@ -266,15 +266,16 @@ export class VMBuilder extends BaseBuilder {
266
266
  const storage = this._storage ?? "rbd_pool";
267
267
  if (template) {
268
268
  console.log(` 📋 Cloning template "${template.name}" (vmid=${template.vmid}) → "${this.name}" (vmid=${newVmid})`);
269
- const taskId = await pm.post(`/nodes/${node}/qemu/${template.vmid}/clone`, {
269
+ const taskId = await pm.post(`/nodes/${template.node || node}/qemu/${template.vmid}/clone`, {
270
270
  newid: newVmid,
271
271
  name: this.name,
272
272
  full: 1,
273
273
  storage,
274
274
  format: "raw",
275
+ target: node,
275
276
  });
276
277
  // Clone is async - wait for the Proxmox task to finish before configuring
277
- await this.waitForTask(node, taskId, pm);
278
+ await this.waitForTask(template.node || node, taskId, pm);
278
279
  }
279
280
  else {
280
281
  console.log(` 🆕 Creating blank VM "${this.name}" (vmid=${newVmid})`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "puls-dev",
3
- "version": "0.2.9",
3
+ "version": "0.3.0",
4
4
  "description": "Intent-driven infrastructure-as-code with eager discovery and no state files.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -101,4 +101,4 @@
101
101
  "reflect-metadata": "^0.2.2",
102
102
  "undici": "^8.3.0"
103
103
  }
104
- }
104
+ }