puls-dev 0.3.2 → 0.3.4

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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from "node:util";
3
+ import { spawn } from "node:child_process";
4
+ import { existsSync } from "node:fs";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { createRequire } from "node:module";
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
10
+ const require = createRequire(import.meta.url);
11
+ function findTsx() {
12
+ let dir = process.cwd();
13
+ while (true) {
14
+ const candidate = path.join(dir, "node_modules", ".bin", "tsx");
15
+ if (existsSync(candidate))
16
+ return candidate;
17
+ const parent = path.dirname(dir);
18
+ if (parent === dir)
19
+ break;
20
+ dir = parent;
21
+ }
22
+ return null;
23
+ }
24
+ function getVersion() {
25
+ try {
26
+ const pkg = require(path.join(__dirname, "../../package.json"));
27
+ return pkg.version;
28
+ }
29
+ catch {
30
+ return "unknown";
31
+ }
32
+ }
33
+ const HELP = `
34
+ Usage:
35
+ puls plan <file> Dry-run the stack — prints what would change, no API writes
36
+ puls deploy <file> Deploy the stack
37
+ puls destroy <file> Destroy the stack
38
+
39
+ Options:
40
+ --parallel Enable parallel resource execution
41
+ --dry-run Force dry-run mode (alias: same as plan)
42
+ --version Print version and exit
43
+ --help Print this help and exit
44
+
45
+ Examples:
46
+ puls plan infra/staging.ts
47
+ puls deploy infra/staging.ts --parallel
48
+ puls destroy infra/staging.ts
49
+ `.trim();
50
+ let parsed;
51
+ try {
52
+ parsed = parseArgs({
53
+ args: process.argv.slice(2),
54
+ options: {
55
+ parallel: { type: "boolean" },
56
+ "dry-run": { type: "boolean" },
57
+ version: { type: "boolean", short: "v" },
58
+ help: { type: "boolean", short: "h" },
59
+ },
60
+ allowPositionals: true,
61
+ strict: true,
62
+ });
63
+ }
64
+ catch (err) {
65
+ console.error(`Error: ${err.message}`);
66
+ console.error('Run "puls --help" for usage.');
67
+ process.exit(1);
68
+ }
69
+ const { values, positionals } = parsed;
70
+ if (values.version) {
71
+ console.log(`puls v${getVersion()}`);
72
+ process.exit(0);
73
+ }
74
+ if (values.help || positionals.length === 0) {
75
+ console.log(HELP);
76
+ process.exit(0);
77
+ }
78
+ const [command, userFile] = positionals;
79
+ const COMMANDS = ["plan", "deploy", "destroy"];
80
+ if (!COMMANDS.includes(command)) {
81
+ console.error(`Error: Unknown command "${command}". Expected: plan, deploy, or destroy.`);
82
+ console.error('Run "puls --help" for usage.');
83
+ process.exit(1);
84
+ }
85
+ if (!userFile) {
86
+ console.error(`Error: Missing file argument.\nUsage: puls ${command} <file>`);
87
+ process.exit(1);
88
+ }
89
+ const resolvedFile = path.resolve(process.cwd(), userFile);
90
+ if (!existsSync(resolvedFile)) {
91
+ console.error(`Error: File not found: ${resolvedFile}`);
92
+ process.exit(1);
93
+ }
94
+ const childEnv = { ...process.env };
95
+ if (command === "plan" || values["dry-run"]) {
96
+ childEnv.PULS_DRY_RUN = "true";
97
+ }
98
+ if (command === "destroy") {
99
+ childEnv.PULS_MODE = "destroy";
100
+ }
101
+ if (values.parallel) {
102
+ childEnv.PULS_PARALLEL = "true";
103
+ }
104
+ const tsxBin = findTsx() ?? "tsx";
105
+ const child = spawn(tsxBin, [resolvedFile], {
106
+ stdio: "inherit",
107
+ env: childEnv,
108
+ });
109
+ child.on("error", (err) => {
110
+ if (err.code === "ENOENT") {
111
+ console.error("Error: Could not find tsx. Install it in your project:\n\n" +
112
+ " npm install --save-dev tsx\n\n" +
113
+ "or globally:\n\n" +
114
+ " npm install -g tsx");
115
+ }
116
+ else {
117
+ console.error(`Error spawning tsx: ${err.message}`);
118
+ }
119
+ process.exit(1);
120
+ });
121
+ child.on("close", (code) => {
122
+ process.exit(code ?? 1);
123
+ });
@@ -9,6 +9,7 @@ export interface GlobalConfig {
9
9
  defaultRegion?: string;
10
10
  spacesAccessKey?: string;
11
11
  spacesSecretKey?: string;
12
+ sshUser?: string;
12
13
  };
13
14
  aws?: {
14
15
  region: string;
@@ -23,6 +24,7 @@ export interface GlobalConfig {
23
24
  dnsDomain?: string;
24
25
  dnsServers?: string[];
25
26
  verifySsl?: boolean;
27
+ sshUser?: string;
26
28
  };
27
29
  firebase?: {
28
30
  projectId: string;
@@ -32,6 +34,7 @@ export interface GlobalConfig {
32
34
  projectId?: string;
33
35
  serviceAccountPath?: string;
34
36
  region?: string;
37
+ sshUser?: string;
35
38
  };
36
39
  };
37
40
  }
@@ -10,5 +10,6 @@ export interface ResourceContext {
10
10
  abortSignal?: AbortSignal;
11
11
  hosts?: HostEntry[];
12
12
  stackName?: string;
13
+ secrets: Set<string>;
13
14
  }
14
15
  export declare const resourceContextStorage: AsyncLocalStorage<ResourceContext>;
@@ -17,6 +17,7 @@ type ProviderOpts = {
17
17
  dnsDomain?: string;
18
18
  dnsServers?: string[];
19
19
  verifySsl?: boolean;
20
+ sshUser?: string;
20
21
  };
21
22
  };
22
23
  export declare function Protected(target: any, propertyKey: string): void;
@@ -23,6 +23,11 @@ function applyConfig(opts) {
23
23
  },
24
24
  });
25
25
  }
26
+ // CLI env-var overrides — applied last so `puls plan/destroy/--parallel` wins over decorator options
27
+ if (process.env.PULS_DRY_RUN === "true")
28
+ Config.set({ dryRun: true });
29
+ if (process.env.PULS_PARALLEL === "true")
30
+ Config.set({ parallel: true });
26
31
  }
27
32
  export function Protected(target, propertyKey) {
28
33
  Reflect.defineMetadata("protected", true, target, propertyKey);
@@ -73,15 +78,22 @@ export function Destroy(optsOrTarget, propertyKey) {
73
78
  export function Deploy(opts = {}) {
74
79
  return function (constructor) {
75
80
  const regions = opts.regions ?? [];
81
+ const mode = process.env.PULS_MODE;
76
82
  if (regions.length > 0) {
77
83
  Promise.resolve().then(async () => {
78
84
  for (const r of regions) {
79
- console.log(`\n🌍 [MULTI-REGION] Deploying stack to region: ${r}`);
85
+ const label = mode === "destroy" ? "Tearing down" : "Deploying";
86
+ console.log(`\n🌍 [MULTI-REGION] ${label} stack in region: ${r}`);
80
87
  applyConfig({ ...opts, region: r });
81
88
  const instance = new constructor();
82
89
  Stack._register(constructor, instance, r);
83
- if (typeof instance.deploy === "function") {
84
- await instance.deploy();
90
+ if (mode === "destroy") {
91
+ if (typeof instance.destroy === "function")
92
+ await instance.destroy();
93
+ }
94
+ else {
95
+ if (typeof instance.deploy === "function")
96
+ await instance.deploy();
85
97
  }
86
98
  }
87
99
  });
@@ -91,8 +103,14 @@ export function Deploy(opts = {}) {
91
103
  const instance = new constructor();
92
104
  Stack._register(constructor, instance);
93
105
  Promise.resolve().then(async () => {
94
- if (typeof instance.deploy === "function")
95
- await instance.deploy();
106
+ if (mode === "destroy") {
107
+ if (typeof instance.destroy === "function")
108
+ await instance.destroy();
109
+ }
110
+ else {
111
+ if (typeof instance.deploy === "function")
112
+ await instance.deploy();
113
+ }
96
114
  });
97
115
  }
98
116
  };
@@ -20,7 +20,14 @@ export class Output {
20
20
  // Transform this output into a new Output<U> without awaiting it yourself.
21
21
  apply(fn) {
22
22
  const out = new Output();
23
- this._promise.then(v => out.resolve(fn(v)), err => out.reject(err));
23
+ this._promise.then(v => {
24
+ try {
25
+ out.resolve(fn(v));
26
+ }
27
+ catch (e) {
28
+ out.reject(e);
29
+ }
30
+ }, err => out.reject(err));
24
31
  return out;
25
32
  }
26
33
  }
@@ -165,6 +165,7 @@ describe("Production Features Unit Tests", () => {
165
165
  test("Ansible Provisioner Stack-Wide Dynamic Inventory Generation", async () => {
166
166
  const context = {
167
167
  stackName: "my-test-stack",
168
+ secrets: new Set(),
168
169
  hosts: [
169
170
  { name: "web1", ip: "1.2.3.4", user: "root", sshKey: "/path/to/key", provider: "do" },
170
171
  { name: "db1", ip: "5.6.7.8", user: "ubuntu", sshKey: "/path/to/other-key", provider: "aws" }
@@ -1,5 +1,6 @@
1
1
  import { Output } from "./output.js";
2
2
  export declare const resolvedSecrets: Set<string>;
3
+ export declare function clearResolvedSecrets(): void;
3
4
  /**
4
5
  * Secret represents a lazy, secure credential that is fetched asynchronously
5
6
  * at deployment time instead of during the eager construction phase.
@@ -1,6 +1,10 @@
1
1
  import { Output } from "./output.js";
2
2
  import { Config } from "./config.js";
3
+ import { resourceContextStorage } from "./context.js";
3
4
  export const resolvedSecrets = new Set();
5
+ export function clearResolvedSecrets() {
6
+ resolvedSecrets.clear();
7
+ }
4
8
  /**
5
9
  * Secret represents a lazy, secure credential that is fetched asynchronously
6
10
  * at deployment time instead of during the eager construction phase.
@@ -25,6 +29,7 @@ export class Secret extends Output {
25
29
  this.resolve(val);
26
30
  if (val && val.length >= 3) {
27
31
  resolvedSecrets.add(val);
32
+ resourceContextStorage.getStore()?.secrets.add(val);
28
33
  }
29
34
  }
30
35
  catch (err) {
@@ -3,6 +3,42 @@ import { BaseBuilder } from "./resource.js";
3
3
  import { Config } from "./config.js";
4
4
  import { resourceContextStorage } from "./context.js";
5
5
  import { resolvedSecrets } from "./secret.js";
6
+ async function withRedactedConsole(secrets, fn) {
7
+ const originalLog = console.log;
8
+ const redact = (message) => {
9
+ if (typeof message !== "string")
10
+ return message;
11
+ let result = message;
12
+ for (const secret of secrets) {
13
+ if (secret && secret.length >= 3) {
14
+ const escaped = secret.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&");
15
+ result = result.replace(new RegExp(escaped, "g"), "********");
16
+ }
17
+ }
18
+ return result;
19
+ };
20
+ console.log = (...args) => {
21
+ const redactedArgs = args.map((arg) => {
22
+ if (typeof arg === "string")
23
+ return redact(arg);
24
+ try {
25
+ const str = String(arg);
26
+ const hasSecret = [...secrets].some((s) => s && s.length >= 3 && str.includes(s));
27
+ if (hasSecret)
28
+ return redact(str);
29
+ }
30
+ catch { }
31
+ return arg;
32
+ });
33
+ originalLog(...redactedArgs);
34
+ };
35
+ try {
36
+ return await fn();
37
+ }
38
+ finally {
39
+ console.log = originalLog;
40
+ }
41
+ }
6
42
  const _registry = new Map();
7
43
  function formatEntry(val, parentKey) {
8
44
  const isSensitiveKey = (k) => /password|secret|token|key/i.test(k);
@@ -102,50 +138,16 @@ export class Stack {
102
138
  async deploy() {
103
139
  const controller = new AbortController();
104
140
  const hosts = [];
141
+ // Snapshot current secrets; new secrets resolved during this run are added via context
142
+ const secrets = new Set(resolvedSecrets);
105
143
  const context = {
106
144
  abortSignal: controller.signal,
107
145
  hosts,
108
- stackName: this.constructor.name
146
+ stackName: this.constructor.name,
147
+ secrets,
109
148
  };
110
149
  return resourceContextStorage.run(context, async () => {
111
- const originalLog = console.log;
112
- console.log = (...args) => {
113
- const redact = (message) => {
114
- if (typeof message !== "string")
115
- return message;
116
- let result = message;
117
- for (const secret of resolvedSecrets) {
118
- if (secret && secret.length >= 3) {
119
- const escaped = secret.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
120
- const regex = new RegExp(escaped, 'g');
121
- result = result.replace(regex, '********');
122
- }
123
- }
124
- return result;
125
- };
126
- const redactedArgs = args.map(arg => {
127
- if (typeof arg === "string") {
128
- return redact(arg);
129
- }
130
- try {
131
- const str = String(arg);
132
- let hasSecret = false;
133
- for (const secret of resolvedSecrets) {
134
- if (secret && secret.length >= 3 && str.includes(secret)) {
135
- hasSecret = true;
136
- break;
137
- }
138
- }
139
- if (hasSecret) {
140
- return redact(str);
141
- }
142
- }
143
- catch { }
144
- return arg;
145
- });
146
- originalLog(...redactedArgs);
147
- };
148
- try {
150
+ return withRedactedConsole(secrets, async () => {
149
151
  console.log(`\n🏗️ Deploying Stack: ${this.constructor.name}`);
150
152
  // Stack-level beforeDeploy hook
151
153
  if (typeof this.beforeDeploy === "function") {
@@ -187,11 +189,13 @@ export class Stack {
187
189
  }
188
190
  // 2. Schedule execution
189
191
  if (isParallel) {
190
- const startPromise = Promise.resolve();
191
192
  const promises = resources.map(({ prop, resource }) => {
192
193
  resource._deployPromise = (async () => {
193
194
  try {
194
- await startPromise;
195
+ // Yield so the map() loop finishes assigning all _deployPromise values before
196
+ // any task checks its dependencies — a dependency that appears later in the list
197
+ // would otherwise have an undefined _deployPromise and be silently skipped.
198
+ await Promise.resolve();
195
199
  if (controller.signal.aborted) {
196
200
  throw new Error("Deployment aborted due to previous failure");
197
201
  }
@@ -287,59 +291,21 @@ export class Stack {
287
291
  await this.afterDeploy(outputs);
288
292
  }
289
293
  return outputs;
290
- }
291
- finally {
292
- console.log = originalLog;
293
- }
294
+ });
294
295
  });
295
296
  }
296
297
  async destroy() {
297
298
  const controller = new AbortController();
298
299
  const hosts = [];
300
+ const secrets = new Set(resolvedSecrets);
299
301
  const context = {
300
302
  abortSignal: controller.signal,
301
303
  hosts,
302
- stackName: this.constructor.name
304
+ stackName: this.constructor.name,
305
+ secrets,
303
306
  };
304
307
  return resourceContextStorage.run(context, async () => {
305
- const originalLog = console.log;
306
- console.log = (...args) => {
307
- const redact = (message) => {
308
- if (typeof message !== "string")
309
- return message;
310
- let result = message;
311
- for (const secret of resolvedSecrets) {
312
- if (secret && secret.length >= 3) {
313
- const escaped = secret.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
314
- const regex = new RegExp(escaped, 'g');
315
- result = result.replace(regex, '********');
316
- }
317
- }
318
- return result;
319
- };
320
- const redactedArgs = args.map(arg => {
321
- if (typeof arg === "string") {
322
- return redact(arg);
323
- }
324
- try {
325
- const str = String(arg);
326
- let hasSecret = false;
327
- for (const secret of resolvedSecrets) {
328
- if (secret && secret.length >= 3 && str.includes(secret)) {
329
- hasSecret = true;
330
- break;
331
- }
332
- }
333
- if (hasSecret) {
334
- return redact(str);
335
- }
336
- }
337
- catch { }
338
- return arg;
339
- });
340
- originalLog(...redactedArgs);
341
- };
342
- try {
308
+ return withRedactedConsole(secrets, async () => {
343
309
  console.log(`\n💥 Tearing down Stack: ${this.constructor.name}`);
344
310
  // Stack-level beforeDestroy hook
345
311
  if (typeof this.beforeDestroy === "function") {
@@ -374,12 +340,13 @@ export class Stack {
374
340
  }
375
341
  // 2. Schedule execution
376
342
  if (isParallel) {
377
- const startPromise = Promise.resolve();
378
343
  // In parallel destroy, await all dependents (reverse dependencies) first
379
344
  const promises = resources.map(({ prop, resource }) => {
380
345
  resource._destroyPromise = (async () => {
381
346
  try {
382
- await startPromise;
347
+ // Yield so the map() loop finishes assigning all _destroyPromise values before
348
+ // any task checks its dependents (same reason as parallel deploy).
349
+ await Promise.resolve();
383
350
  if (controller.signal.aborted) {
384
351
  throw new Error("Teardown aborted due to previous failure");
385
352
  }
@@ -458,10 +425,7 @@ export class Stack {
458
425
  await this.afterDestroy(outputs);
459
426
  }
460
427
  return outputs;
461
- }
462
- finally {
463
- console.log = originalLog;
464
- }
428
+ });
465
429
  });
466
430
  }
467
431
  }
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ export * from "./core/stack.js";
2
2
  export * from "./core/decorators.js";
3
3
  export * from "./core/checker.js";
4
4
  export * from "./core/resource.js";
5
- export { Secret } from "./core/secret.js";
5
+ export { Secret, clearResolvedSecrets } from "./core/secret.js";
6
6
  export { Output } from "./core/output.js";
7
7
  export * as INVENTORY_TYPES from "./types/inventory.js";
8
8
  export { SLACK, DISCORD } from "./core/hooks.js";
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ export * from "./core/stack.js";
2
2
  export * from "./core/decorators.js";
3
3
  export * from "./core/checker.js";
4
4
  export * from "./core/resource.js";
5
- export { Secret } from "./core/secret.js";
5
+ export { Secret, clearResolvedSecrets } from "./core/secret.js";
6
6
  export { Output } from "./core/output.js";
7
7
  export * as INVENTORY_TYPES from "./types/inventory.js";
8
8
  export { SLACK, DISCORD } from "./core/hooks.js";
@@ -12,6 +12,7 @@ export declare class DropletBuilder extends BaseBuilder {
12
12
  private dropletId?;
13
13
  private resolvedIp?;
14
14
  private sshKeyPath?;
15
+ private _sshUser?;
15
16
  private _provision;
16
17
  private _forceConfigCheck;
17
18
  constructor(name: string);
@@ -21,7 +22,11 @@ export declare class DropletBuilder extends BaseBuilder {
21
22
  image(image: (typeof OS)[keyof typeof OS] | string): this;
22
23
  region(region: (typeof REGION)[keyof typeof REGION] | string): this;
23
24
  size(size: (typeof SIZE)[keyof typeof SIZE] | string): this;
25
+ sshKey(keyPath: string): this;
26
+ /** @deprecated Use `.sshKey()` instead */
24
27
  sslKey(keyPath: string): this;
28
+ sshUser(user: string): this;
29
+ private resolveUser;
25
30
  vpc(uuid: string | Output<string>): this;
26
31
  provision(...playbookPaths: (string | string[])[]): this;
27
32
  forceConfigCheck(): this;
@@ -24,6 +24,7 @@ export class DropletBuilder extends BaseBuilder {
24
24
  dropletId;
25
25
  resolvedIp;
26
26
  sshKeyPath;
27
+ _sshUser;
27
28
  _provision = [];
28
29
  _forceConfigCheck = false;
29
30
  constructor(name) {
@@ -70,10 +71,24 @@ export class DropletBuilder extends BaseBuilder {
70
71
  this.config.size = size;
71
72
  return this;
72
73
  }
73
- sslKey(keyPath) {
74
+ sshKey(keyPath) {
74
75
  this.sshKeyPath = keyPath.replace('~', homedir());
75
76
  return this;
76
77
  }
78
+ /** @deprecated Use `.sshKey()` instead */
79
+ sslKey(keyPath) {
80
+ return this.sshKey(keyPath);
81
+ }
82
+ sshUser(user) {
83
+ this._sshUser = user;
84
+ return this;
85
+ }
86
+ resolveUser() {
87
+ return (this._sshUser ??
88
+ process.env.DO_SSH_USER ??
89
+ Config.get().providers.do?.sshUser ??
90
+ "root");
91
+ }
77
92
  vpc(uuid) {
78
93
  this.config.vpc_uuid = uuid;
79
94
  return this;
@@ -91,7 +106,7 @@ export class DropletBuilder extends BaseBuilder {
91
106
  }
92
107
  async runProvisioner(ip, script) {
93
108
  const keyPath = this.sshKeyPath ? this.sshKeyPath : undefined;
94
- return runProvisioner(ip, "root", keyPath, script);
109
+ return runProvisioner(ip, this.resolveUser(), keyPath, script);
95
110
  }
96
111
  async resolveOrRegisterSshKey(api) {
97
112
  const pubPath = this.sshKeyPath.replace(/\.pub$/, '') + '.pub';
@@ -258,7 +273,7 @@ export class DropletBuilder extends BaseBuilder {
258
273
  context.hosts.push({
259
274
  name: this.name,
260
275
  ip: activeIp,
261
- user: "root",
276
+ user: this.resolveUser(),
262
277
  sshKey: this.sshKeyPath,
263
278
  provider: "do"
264
279
  });
@@ -106,7 +106,7 @@ describe('DropletBuilder Unit Tests', () => {
106
106
  builder
107
107
  .region('nyc3')
108
108
  .size('s-1vcpu-1gb')
109
- .sslKey('~/.ssh/id_rsa.pub');
109
+ .sshKey('~/.ssh/id_rsa.pub');
110
110
  const result = await builder.deploy();
111
111
  assert.ok(result);
112
112
  assert.strictEqual(result.region, 'nyc3');
@@ -9,6 +9,7 @@ export declare class GCPTemplateBuilder extends BaseBuilder {
9
9
  private _zone;
10
10
  private _network;
11
11
  private _sshKeys;
12
+ private _sshUser?;
12
13
  private _provision;
13
14
  constructor(name: string);
14
15
  baseImage(img: string): this;
@@ -16,6 +17,8 @@ export declare class GCPTemplateBuilder extends BaseBuilder {
16
17
  zone(z: string): this;
17
18
  network(netPath: string): this;
18
19
  sshKey(keys: string | string[]): this;
20
+ sshUser(user: string): this;
21
+ private resolveUser;
19
22
  provision(...playbookPaths: (string | string[])[]): this;
20
23
  private discoverImage;
21
24
  protected checkPort(ip: string, port: number): Promise<boolean>;
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { BaseBuilder } from "../../core/resource.js";
4
+ import { Config } from "../../core/config.js";
4
5
  import { Output } from "../../core/output.js";
5
6
  import { gcpFetch, getProjectId } from "./api.js";
6
7
  import { checkPort, runProvisioner } from "../../core/provisioner.js";
@@ -15,6 +16,7 @@ export class GCPTemplateBuilder extends BaseBuilder {
15
16
  _zone = "us-central1-a";
16
17
  _network = "global/networks/default";
17
18
  _sshKeys = [];
19
+ _sshUser;
18
20
  _provision = [];
19
21
  constructor(name) {
20
22
  super(name);
@@ -41,6 +43,16 @@ export class GCPTemplateBuilder extends BaseBuilder {
41
43
  this._sshKeys = keys;
42
44
  return this;
43
45
  }
46
+ sshUser(user) {
47
+ this._sshUser = user;
48
+ return this;
49
+ }
50
+ resolveUser() {
51
+ return (this._sshUser ??
52
+ process.env.GCP_SSH_USER ??
53
+ Config.get().providers.gcp?.sshUser ??
54
+ "root");
55
+ }
44
56
  provision(...playbookPaths) {
45
57
  this._provision.push(...playbookPaths.flat());
46
58
  return this;
@@ -66,7 +78,7 @@ export class GCPTemplateBuilder extends BaseBuilder {
66
78
  async runProvisioner(ip, script) {
67
79
  const keysArray = Array.isArray(this._sshKeys) ? this._sshKeys : [this._sshKeys];
68
80
  const keyPath = keysArray.find(k => !k.startsWith('ssh-') && !k.startsWith('ecdsa-') && !k.startsWith('sk-'));
69
- return runProvisioner(ip, "root", keyPath, script);
81
+ return runProvisioner(ip, this.resolveUser(), keyPath, script);
70
82
  }
71
83
  async deploy() {
72
84
  const dryRun = this.isDryRunActive();
@@ -12,6 +12,7 @@ export declare class GCPVMBuilder extends BaseBuilder {
12
12
  private _zone;
13
13
  private _network;
14
14
  private _sshKeys;
15
+ private _sshUser?;
15
16
  private _provision;
16
17
  private _forceConfigCheck;
17
18
  private resolvedInstanceId?;
@@ -23,6 +24,8 @@ export declare class GCPVMBuilder extends BaseBuilder {
23
24
  zone(z: string): this;
24
25
  network(netPath: string): this;
25
26
  sshKey(keys: string | string[]): this;
27
+ sshUser(user: string): this;
28
+ private resolveUser;
26
29
  provision(...playbookPaths: (string | string[])[]): this;
27
30
  forceConfigCheck(): this;
28
31
  protected checkPort(ip: string, port: number): Promise<boolean>;