badmfck-api-server 5.0.3 → 5.0.6

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.
@@ -9,6 +9,7 @@ const path_1 = __importDefault(require("path"));
9
9
  const fs_1 = __importDefault(require("fs"));
10
10
  const __1 = require("../..");
11
11
  const crypto_1 = __importDefault(require("crypto"));
12
+ const os_1 = __importDefault(require("os"));
12
13
  const undici_1 = require("undici");
13
14
  const resolveConfig_1 = require("./resolveConfig");
14
15
  async function Deploy(opt) {
@@ -16,23 +17,28 @@ async function Deploy(opt) {
16
17
  const archiveName = __1.UID.sha256(opt.name.replaceAll(".", "_")) + ".tar.gz";
17
18
  console.log("Changing Config to live");
18
19
  const config = path_1.default.resolve("src", "Config.ts");
19
- let configSrc = fs_1.default.readFileSync(config).toString("utf-8");
20
- const schemeRe = /(\bscheme\s*:\s*Scheme\s*=\s*)local\b/g;
21
- let replaced = 0;
22
- const updated = configSrc.replace(schemeRe, (_match, prefix) => {
23
- replaced++;
24
- return prefix + "live";
25
- });
26
- if (replaced > 0) {
27
- fs_1.default.writeFileSync(config, updated);
28
- console.log(`Config: switched to live (${replaced} occurrence${replaced === 1 ? "" : "s"})`);
29
- }
30
- else if (/\bscheme\s*:\s*Scheme\s*=\s*live\b/.test(configSrc)) {
31
- console.log("Config already in live mode — nothing to change");
20
+ if (!fs_1.default.existsSync(config)) {
21
+ console.log("Config: src/Config.ts not found — skipping scheme switch");
32
22
  }
33
23
  else {
34
- console.warn("Config: no 'scheme: Scheme = local|live' pattern found in " + config +
35
- " — check the file was not renamed/restructured");
24
+ const configSrc = fs_1.default.readFileSync(config).toString("utf-8");
25
+ const schemeRe = /(\bscheme\s*:\s*Scheme\s*=\s*)local\b/g;
26
+ let replaced = 0;
27
+ const updated = configSrc.replace(schemeRe, (_match, prefix) => {
28
+ replaced++;
29
+ return prefix + "live";
30
+ });
31
+ if (replaced > 0) {
32
+ fs_1.default.writeFileSync(config, updated);
33
+ console.log(`Config: switched to live (${replaced} occurrence${replaced === 1 ? "" : "s"})`);
34
+ }
35
+ else if (/\bscheme\s*:\s*Scheme\s*=\s*live\b/.test(configSrc)) {
36
+ console.log("Config already in live mode — nothing to change");
37
+ }
38
+ else {
39
+ console.warn("Config: no 'scheme: Scheme = local|live' pattern found in " + config +
40
+ " — check the file was not renamed/restructured");
41
+ }
36
42
  }
37
43
  const pkgPath = path_1.default.resolve("package.json");
38
44
  const pkg = JSON.parse(fs_1.default.readFileSync(pkgPath).toString("utf-8"));
@@ -69,21 +75,33 @@ async function Deploy(opt) {
69
75
  spaMode = true;
70
76
  console.log("No ./bin found — using entire ./dist contents as distribution");
71
77
  }
78
+ const staged = [];
72
79
  if (Array.isArray(opt.includes)) {
73
- for (const inc of opt.includes) {
74
- if (typeof inc !== "string" || inc.length === 0)
80
+ for (const raw of opt.includes) {
81
+ const inc = typeof raw === "string" ? { src: raw, dest: raw } : raw;
82
+ if (!inc || typeof inc.src !== "string" || inc.src.length === 0)
75
83
  continue;
76
- if (inc.startsWith("-")) {
77
- console.warn(`Skipping include that starts with '-': ${inc}`);
84
+ const src = inc.src;
85
+ const dest = (inc.dest && inc.dest.length > 0 ? inc.dest : src).replace(/^\.\//, "");
86
+ if (src.startsWith("-") || dest.startsWith("-"))
87
+ throw new Error(`Include path must not start with '-': ${src} -> ${dest}`);
88
+ if (!fs_1.default.existsSync(src))
89
+ throw new Error(`Include source does not exist: ${src}`);
90
+ if (path_1.default.isAbsolute(dest) || dest.split(/[\\/]/).includes(".."))
91
+ throw new Error(`Include 'dest' must be a relative path inside the slot, without '..': ${dest}`);
92
+ if (dest === src.replace(/^\.\//, "")) {
93
+ items.push(src);
78
94
  continue;
79
95
  }
80
- if (!fs_1.default.existsSync(inc)) {
81
- console.warn(`Include path does not exist (tar will error): ${inc}`);
82
- }
83
- items.push(inc);
96
+ staged.push({ src, dest });
84
97
  }
85
98
  }
86
- if (items.length === 0 && !spaMode) {
99
+ for (const s of staged) {
100
+ const clash = items.some(i => i.replace(/^\.\//, "") === s.dest);
101
+ if (clash)
102
+ console.warn(`Include dest "${s.dest}" collides with an already-archived path — the later entry wins on extraction`);
103
+ }
104
+ if (items.length === 0 && !spaMode && staged.length === 0) {
87
105
  throw new Error("Nothing to archive (no ./bin, no ./dist/index.html, no valid includes)");
88
106
  }
89
107
  const excludes = new Set();
@@ -99,6 +117,8 @@ async function Deploy(opt) {
99
117
  }
100
118
  excludes.add(trimmed);
101
119
  };
120
+ addExclude("._*", "built-in");
121
+ addExclude(".DS_Store", "built-in");
102
122
  const ignoreFile = path_1.default.resolve(".deployignore");
103
123
  if (fs_1.default.existsSync(ignoreFile)) {
104
124
  const lines = fs_1.default.readFileSync(ignoreFile, "utf-8").split(/\r?\n/);
@@ -120,6 +140,17 @@ async function Deploy(opt) {
120
140
  else {
121
141
  tarArgs.push(...items);
122
142
  }
143
+ let stageDir = null;
144
+ if (staged.length > 0) {
145
+ stageDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), "badmfck-deploy-"));
146
+ for (const { src, dest } of staged) {
147
+ const target = path_1.default.join(stageDir, dest);
148
+ fs_1.default.mkdirSync(path_1.default.dirname(target), { recursive: true });
149
+ fs_1.default.cpSync(path_1.default.resolve(src), target, { recursive: true });
150
+ console.log(`Include: ${src} -> ${dest}`);
151
+ }
152
+ tarArgs.push("-C", stageDir, ".");
153
+ }
123
154
  if (spaMode) {
124
155
  console.log("Archiving: contents of ./dist (recursively, at archive root)");
125
156
  if (items.length > 0)
@@ -131,7 +162,11 @@ async function Deploy(opt) {
131
162
  if (excludeArgs.length > 0)
132
163
  console.log("Excluding:", Array.from(excludes).join(", "));
133
164
  try {
134
- (0, child_process_1.execFileSync)("tar", tarArgs, { encoding: "utf-8", stdio: "inherit" });
165
+ (0, child_process_1.execFileSync)("tar", tarArgs, {
166
+ encoding: "utf-8",
167
+ stdio: "inherit",
168
+ env: { ...process.env, COPYFILE_DISABLE: "1" },
169
+ });
135
170
  }
136
171
  catch (err) {
137
172
  console.error(`\ntar failed while creating ${archiveName}\n`);
@@ -143,6 +178,16 @@ async function Deploy(opt) {
143
178
  console.error(errOut);
144
179
  process.exit(typeof err.status === "number" ? err.status : 1);
145
180
  }
181
+ finally {
182
+ if (stageDir) {
183
+ try {
184
+ fs_1.default.rmSync(stageDir, { recursive: true, force: true });
185
+ }
186
+ catch (e) {
187
+ console.warn(`Could not remove staging dir ${stageDir}: ${e?.message ?? e}`);
188
+ }
189
+ }
190
+ }
146
191
  if (!fs_1.default.existsSync(path_1.default.resolve(archiveName))) {
147
192
  console.error("ARCHIVE NOT CREATED!");
148
193
  return "";
@@ -6,4 +6,5 @@ export declare class Deployer extends BaseEndpoint {
6
6
  file(req: HTTPRequestVO): Promise<TransferPacketVO<any>>;
7
7
  switchSlot(req: HTTPRequestVO): Promise<TransferPacketVO<any>>;
8
8
  status(req: HTTPRequestVO): Promise<TransferPacketVO<any>>;
9
+ slot(req: HTTPRequestVO): Promise<TransferPacketVO<any>>;
9
10
  }
@@ -41,6 +41,19 @@ class Deployer extends BaseEndpoint_1.BaseEndpoint {
41
41
  name: "",
42
42
  $__name_optional: true
43
43
  }
44
+ },
45
+ {
46
+ ignoreInterceptor: true,
47
+ ignoreHttpLogging: true,
48
+ endpoint: "slot",
49
+ handler: this.slot,
50
+ validationModel: {
51
+ name: "",
52
+ token: "",
53
+ slot: "",
54
+ $__slot_values: ["blue", "green"],
55
+ $__slot_optional: true
56
+ }
44
57
  }
45
58
  ]);
46
59
  }
@@ -56,5 +69,8 @@ class Deployer extends BaseEndpoint_1.BaseEndpoint {
56
69
  async status(req) {
57
70
  return await DeployerService_1.REQ_DEPLOYMENT_STATUS.request({ ...req.data, auth: req.headers.authorization });
58
71
  }
72
+ async slot(req) {
73
+ return await DeployerService_1.REQ_DEPLOYMENT_SLOT.request({ name: req.data.name, token: req.data.token, slot: req.data.slot });
74
+ }
59
75
  }
60
76
  exports.Deployer = Deployer;
@@ -17,11 +17,15 @@ declare const _TSetup: {
17
17
  readonly KEY: "";
18
18
  };
19
19
  readonly $__notifier_optional: true;
20
+ readonly status_url: "";
21
+ readonly $__status_url_optional: true;
20
22
  };
21
23
  export type TDeployerSetup = ValidationModel<typeof _TSetup>;
22
24
  export interface IDeployerServiceOptions {
23
25
  projects_path: string;
24
26
  setup_path: string;
27
+ selfPort?: number;
28
+ selfBaseEndPoint?: string;
25
29
  }
26
30
  export declare const REQ_DEPLOYMENT_PROCEED: Req<{
27
31
  auth: string;
@@ -41,7 +45,14 @@ export declare const REQ_DEPLOYMENT_STATUS: Req<{
41
45
  auth?: string;
42
46
  name?: string;
43
47
  }, any>;
48
+ export declare const REQ_DEPLOYMENT_SLOT: Req<{
49
+ name: string;
50
+ token: string;
51
+ slot?: "blue" | "green";
52
+ }, any>;
44
53
  export declare class DeployerService extends BaseService {
54
+ private static MAX_HISTORY;
55
+ private static _trimHistory;
45
56
  options: IDeployerServiceOptions;
46
57
  setup: TDeployerSetup | null;
47
58
  notifier: IDeploymentNotifier | null;
@@ -117,6 +128,29 @@ export declare class DeployerService extends BaseService {
117
128
  readyToSwitch: boolean;
118
129
  }>;
119
130
  }>;
131
+ slotState(data: {
132
+ name: string;
133
+ token: string;
134
+ slot?: "blue" | "green";
135
+ }): Promise<{
136
+ error: {
137
+ code: number;
138
+ message: string;
139
+ httpStatus: number;
140
+ };
141
+ } | {
142
+ data: {
143
+ project: string;
144
+ active: "blue" | "green";
145
+ lastDeployed: "blue" | "green";
146
+ readyToSwitch: boolean;
147
+ slot: "blue" | "green" | null;
148
+ isActive: boolean | null;
149
+ checkedAt: number;
150
+ };
151
+ }>;
152
+ private _statusUrl;
153
+ private _writeBlueGreenFiles;
120
154
  tokensMatch(a: string | undefined, b: string | undefined): boolean;
121
155
  private _checkUserAuth;
122
156
  private static _extractPort;
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.DeployerService = exports.REQ_DEPLOYMENT_STATUS = exports.REQ_DEPLOYMENT_SWITCH = exports.REQ_DEPLOYMENT_PROCEED = void 0;
6
+ exports.DeployerService = exports.REQ_DEPLOYMENT_SLOT = exports.REQ_DEPLOYMENT_STATUS = exports.REQ_DEPLOYMENT_SWITCH = exports.REQ_DEPLOYMENT_PROCEED = void 0;
7
7
  const badmfck_signal_1 = require("badmfck-signal");
8
8
  const BaseService_1 = require("../BaseService");
9
9
  const ConfigService_1 = require("./ConfigService");
@@ -16,6 +16,7 @@ const fs_1 = __importDefault(require("fs"));
16
16
  const crypto_1 = __importDefault(require("crypto"));
17
17
  const Watchdog_1 = require("./Watchdog");
18
18
  const NginxHelper_1 = require("./NginxHelper");
19
+ const BlueGreenState_1 = require("./BlueGreenState");
19
20
  const _TSetup = {
20
21
  users: [{
21
22
  login: "",
@@ -27,12 +28,21 @@ const _TSetup = {
27
28
  URL: "",
28
29
  KEY: "",
29
30
  },
30
- $__notifier_optional: true
31
+ $__notifier_optional: true,
32
+ status_url: "",
33
+ $__status_url_optional: true
31
34
  };
32
35
  exports.REQ_DEPLOYMENT_PROCEED = new badmfck_signal_1.Req(undefined, "REQ_DEPLOYER_SERVICE");
33
36
  exports.REQ_DEPLOYMENT_SWITCH = new badmfck_signal_1.Req(undefined, "REQ_DEPLOYER_SWITCH");
34
37
  exports.REQ_DEPLOYMENT_STATUS = new badmfck_signal_1.Req(undefined, "REQ_DEPLOYER_STATUS");
38
+ exports.REQ_DEPLOYMENT_SLOT = new badmfck_signal_1.Req(undefined, "REQ_DEPLOYER_SLOT");
35
39
  class DeployerService extends BaseService_1.BaseService {
40
+ static MAX_HISTORY = 200;
41
+ static _trimHistory(state) {
42
+ const h = state.history;
43
+ if (Array.isArray(h) && h.length > DeployerService.MAX_HISTORY)
44
+ h.splice(0, h.length - DeployerService.MAX_HISTORY);
45
+ }
36
46
  options;
37
47
  setup = null;
38
48
  notifier = null;
@@ -65,6 +75,7 @@ class DeployerService extends BaseService_1.BaseService {
65
75
  exports.REQ_DEPLOYMENT_PROCEED.listener = async (data) => this.proceed(data);
66
76
  exports.REQ_DEPLOYMENT_SWITCH.listener = async (data) => this.switchSlot(data);
67
77
  exports.REQ_DEPLOYMENT_STATUS.listener = async (data) => this.status(data);
78
+ exports.REQ_DEPLOYMENT_SLOT.listener = async (data) => this.slotState(data);
68
79
  }
69
80
  async _loadSetup() {
70
81
  const setupFile = path_1.default.resolve(this.options.setup_path, "setup.json");
@@ -176,12 +187,15 @@ class DeployerService extends BaseService_1.BaseService {
176
187
  else {
177
188
  projectState.lastDeployed = projectState.active === "blue" ? "green" : "blue";
178
189
  }
190
+ if (!projectState.statusToken)
191
+ projectState.statusToken = crypto_1.default.randomBytes(32).toString("hex");
179
192
  projectState.history.push({
180
193
  slot: projectState.lastDeployed,
181
194
  action: "deploy",
182
195
  user: user.login,
183
196
  at: Date.now()
184
197
  });
198
+ DeployerService._trimHistory(projectState);
185
199
  const subdir = projectState.lastDeployed;
186
200
  const deployPath = path_1.default.resolve(found.destination, subdir);
187
201
  fs_1.default.mkdirSync(deployPath, { recursive: true });
@@ -233,6 +247,9 @@ class DeployerService extends BaseService_1.BaseService {
233
247
  if (found.config) {
234
248
  await this.applyConfig(found.destination, found.config);
235
249
  }
250
+ if (projectState && projectStatePath) {
251
+ this._writeBlueGreenFiles(data.name, projectState, path_1.default.dirname(found.destination));
252
+ }
236
253
  if (found.pm2) {
237
254
  const pm2Result = await this.runPM2(found, projectState);
238
255
  if (__1.ErrorUtils.isError(pm2Result)) {
@@ -359,6 +376,7 @@ class DeployerService extends BaseService_1.BaseService {
359
376
  user: user.login,
360
377
  at: Date.now(),
361
378
  });
379
+ DeployerService._trimHistory(projectState);
362
380
  const tmp = projectStatePath + ".tmp";
363
381
  fs_1.default.writeFileSync(tmp, JSON.stringify(projectState, null, 4), "utf-8");
364
382
  fs_1.default.renameSync(tmp, projectStatePath);
@@ -418,6 +436,64 @@ class DeployerService extends BaseService_1.BaseService {
418
436
  }
419
437
  return { data: results };
420
438
  }
439
+ async slotState(data) {
440
+ const deny = { error: { code: 53, message: "Invalid project or status token.", httpStatus: 401 } };
441
+ if (!data || typeof data.name !== "string" || typeof data.token !== "string" || !/^[A-Za-z0-9._-]+$/.test(data.name))
442
+ return deny;
443
+ let state = null;
444
+ try {
445
+ const statePath = path_1.default.resolve(this.options.projects_path, data.name + "_state_.json");
446
+ if (fs_1.default.existsSync(statePath))
447
+ state = JSON.parse(fs_1.default.readFileSync(statePath, "utf-8"));
448
+ }
449
+ catch {
450
+ state = null;
451
+ }
452
+ const tokenOk = this.tokensMatch(data.token, state?.statusToken || crypto_1.default.randomBytes(32).toString("hex"));
453
+ if (!state || !state.statusToken || !tokenOk)
454
+ return deny;
455
+ const slot = data.slot === "blue" || data.slot === "green" ? data.slot : null;
456
+ return { data: {
457
+ project: data.name,
458
+ active: state.active ?? null,
459
+ lastDeployed: state.lastDeployed ?? null,
460
+ readyToSwitch: !!(state.lastDeployed && state.lastDeployed !== state.active),
461
+ slot,
462
+ isActive: slot ? slot === state.active : null,
463
+ checkedAt: Date.now(),
464
+ } };
465
+ }
466
+ _statusUrl() {
467
+ const override = this.setup?.status_url;
468
+ if (typeof override === "string" && override.length > 0)
469
+ return override;
470
+ let base = this.options.selfBaseEndPoint ?? "/api/";
471
+ if (!base.startsWith("/"))
472
+ base = "/" + base;
473
+ if (!base.endsWith("/"))
474
+ base += "/";
475
+ return `http://127.0.0.1:${this.options.selfPort ?? 8080}${base}pckg/slot`;
476
+ }
477
+ _writeBlueGreenFiles(project, state, projectRoot) {
478
+ if (!state.statusToken)
479
+ return;
480
+ const statusUrl = this._statusUrl();
481
+ for (const slot of ["blue", "green"]) {
482
+ const dir = path_1.default.resolve(projectRoot, slot);
483
+ if (!fs_1.default.existsSync(dir))
484
+ continue;
485
+ const file = path_1.default.resolve(dir, BlueGreenState_1.BLUEGREEN_FILE);
486
+ const payload = { version: 1, project, slot, statusUrl, token: state.statusToken, writtenAt: Date.now() };
487
+ try {
488
+ const tmp = file + ".tmp";
489
+ fs_1.default.writeFileSync(tmp, JSON.stringify(payload, null, 2), { encoding: "utf-8", mode: 0o600 });
490
+ fs_1.default.renameSync(tmp, file);
491
+ }
492
+ catch (e) {
493
+ (0, LogService_1.logWarn)(`Cannot write ${BlueGreenState_1.BLUEGREEN_FILE} for ${project}/${slot}: ${e?.message ?? e}`);
494
+ }
495
+ }
496
+ }
421
497
  tokensMatch(a, b) {
422
498
  const ha = new Uint8Array(crypto_1.default.createHash("sha256").update(String(a ?? "")).digest());
423
499
  const hb = new Uint8Array(crypto_1.default.createHash("sha256").update(String(b ?? "")).digest());
@@ -0,0 +1,2 @@
1
+ import { IDeployerConfig } from "./resolveConfig";
2
+ export declare function Status(opt?: IDeployerConfig | string | null): Promise<string | null>;
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.Status = Status;
7
+ const crypto_1 = __importDefault(require("crypto"));
8
+ const __1 = require("../..");
9
+ const resolveConfig_1 = require("./resolveConfig");
10
+ async function Status(opt) {
11
+ opt = await (0, resolveConfig_1.resolveConfig)(opt);
12
+ let statusUrl;
13
+ if (/\/add\/?$/.test(opt.host))
14
+ statusUrl = opt.host.replace(/\/add\/?$/, "/status");
15
+ else if (opt.switch_host && /\/switch\/?$/.test(opt.switch_host))
16
+ statusUrl = opt.switch_host.replace(/\/switch\/?$/, "/status");
17
+ else
18
+ throw new Error(`Cannot derive status URL from host "${opt.host}" — make sure "host" ends with /add.`);
19
+ const authToken = crypto_1.default.createHash("sha256").update(opt.username + ":" + opt.password).digest("hex");
20
+ const response = await __1.Http.post(statusUrl, { name: opt.name }, {
21
+ headers: { authorization: `Bearer ${authToken}` },
22
+ timeoutMs: 15 * 1000,
23
+ retry: { enabled: false },
24
+ });
25
+ const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
26
+ const c = (code, t) => useColor ? `\x1b[${code}m${t}\x1b[0m` : t;
27
+ const red = (t) => c("31", t), green = (t) => c("32", t), yellow = (t) => c("33", t);
28
+ const blue = (t) => c("34", t), dim = (t) => c("2", t), bold = (t) => c("1", t);
29
+ const slotColor = (s) => s === "blue" ? blue(bold("BLUE")) : s === "green" ? green(bold("GREEN")) : dim("—");
30
+ const line = dim("─".repeat(60));
31
+ console.log("");
32
+ if (!response.ok) {
33
+ console.error(red(bold("✗ STATUS FAILED — transport error")));
34
+ console.error(line);
35
+ console.error(` ${dim("host: ")} ${statusUrl}`);
36
+ console.error(` ${dim("status:")} ${response.status !== undefined ? `HTTP ${response.status}` : "network error (no response)"}`);
37
+ if (response.error !== undefined) {
38
+ const t = typeof response.error === "string" ? response.error : JSON.stringify(response.error);
39
+ console.error(` ${dim("error: ")} ${t.length > 400 ? t.slice(0, 400) + "…" : t}`);
40
+ }
41
+ console.error(line);
42
+ process.exit(1);
43
+ }
44
+ const body = response.data;
45
+ const appError = __1.ErrorUtils.isError(body?.error) ? body.error : __1.ErrorUtils.isError(body?.data) ? body.data : null;
46
+ if (appError) {
47
+ console.error(red(bold("✗ STATUS REJECTED BY SERVER")));
48
+ console.error(line);
49
+ console.error(` ${dim("code: ")} ${appError.code}`);
50
+ console.error(` ${dim("message:")} ${appError.message}`);
51
+ console.error(line);
52
+ process.exit(1);
53
+ }
54
+ const entry = body?.data?.[opt.name];
55
+ if (!entry) {
56
+ console.error(red(bold(`✗ Project "${opt.name}" is not configured on the deployer`)));
57
+ process.exit(1);
58
+ }
59
+ const state = entry.state;
60
+ console.log(`${bold("●")} ${bold(opt.name)}`);
61
+ console.log(line);
62
+ if (!entry.hasBlueGreen) {
63
+ console.log(` ${dim("mode: ")} single slot ${dim("(no blue-green configured)")}`);
64
+ }
65
+ else if (!state) {
66
+ console.log(` ${dim("mode: ")} blue-green`);
67
+ console.log(` ${dim("state: ")} ${yellow("never deployed")}`);
68
+ }
69
+ else {
70
+ console.log(` ${dim("active: ")} ${slotColor(state.active)}`);
71
+ console.log(` ${dim("lastDeployed:")} ${slotColor(state.lastDeployed)}`);
72
+ if (entry.readyToSwitch)
73
+ console.log(` ${yellow("⚠ " + state.lastDeployed + " is deployed but NOT switched")} ${dim("→ badmfck activate")}`);
74
+ const last = Array.isArray(state.history) && state.history.length ? state.history[state.history.length - 1] : null;
75
+ if (last) {
76
+ const d = new Date(last.at);
77
+ const pad = (n) => String(n).padStart(2, "0");
78
+ const when = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
79
+ console.log(` ${dim("last action: ")} ${last.action} ${last.slot} ${dim("by")} ${last.user} ${dim("·")} ${when}`);
80
+ }
81
+ }
82
+ console.log(line);
83
+ console.log("");
84
+ return state?.active ?? null;
85
+ }
@@ -3,10 +3,12 @@
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  const Deploy_1 = require("./Deploy");
5
5
  const Activate_1 = require("./Activate");
6
+ const Status_1 = require("./Status");
6
7
  function usage() {
7
8
  console.error("Usage:");
8
9
  console.error(" badmfck deploy [config.json] — build + upload the current project");
9
10
  console.error(" badmfck activate [config.json] [blue|green] — switch blue-green slot (omit slot for lastDeployed)");
11
+ console.error(" badmfck status [config.json] — show active / last deployed slot");
10
12
  console.error("");
11
13
  console.error("Config path is optional (auto-discovers deploy/ folder → deploy.json when omitted).");
12
14
  console.error("Via npm, prefix args with -- e.g. npm run deploy -- deploy/custom.json");
@@ -51,6 +53,15 @@ async function main() {
51
53
  await (0, Activate_1.Activate)(config, scheme);
52
54
  break;
53
55
  }
56
+ case "status": {
57
+ const { scheme, config } = parseArgs(rest);
58
+ if (scheme) {
59
+ console.error(`"status" does not take a slot argument ("${scheme}").`);
60
+ process.exit(1);
61
+ }
62
+ await (0, Status_1.Status)(config);
63
+ break;
64
+ }
54
65
  default:
55
66
  if (command)
56
67
  console.error(`Unknown command: ${command}\n`);
@@ -4,7 +4,10 @@ export interface IDeployerConfig {
4
4
  host: string;
5
5
  username: string;
6
6
  password: string;
7
- includes?: string[];
7
+ includes?: (string | {
8
+ src: string;
9
+ dest?: string;
10
+ })[];
8
11
  excludes?: string[];
9
12
  switch_host?: string;
10
13
  }
@@ -14,8 +14,20 @@ function validateConfig(cfg) {
14
14
  return "not an object";
15
15
  if (!cfg.name || !cfg.token || !cfg.host || !cfg.username || !cfg.password)
16
16
  return "missing one of: name, token, host, username, password";
17
- if (cfg.includes && !Array.isArray(cfg.includes))
18
- return "includes must be an array";
17
+ if (cfg.includes) {
18
+ if (!Array.isArray(cfg.includes))
19
+ return "includes must be an array";
20
+ for (const inc of cfg.includes) {
21
+ if (typeof inc === "string")
22
+ continue;
23
+ if (!inc || typeof inc !== "object")
24
+ return "includes entries must be a string or { src, dest? }";
25
+ if (typeof inc.src !== "string" || inc.src.length === 0)
26
+ return "includes entry is missing a non-empty 'src'";
27
+ if (inc.dest !== undefined && (typeof inc.dest !== "string" || inc.dest.length === 0))
28
+ return `includes entry "${inc.src}" has an invalid 'dest'`;
29
+ }
30
+ }
19
31
  if (cfg.excludes && !Array.isArray(cfg.excludes))
20
32
  return "excludes must be an array";
21
33
  if (cfg.switch_host && typeof cfg.switch_host !== "string")
@@ -7,6 +7,7 @@ exports.Http = void 0;
7
7
  const undici_1 = require("undici");
8
8
  const node_diagnostics_channel_1 = __importDefault(require("node:diagnostics_channel"));
9
9
  const MonitorService_1 = require("../MonitorService");
10
+ const LogService_1 = require("../LogService");
10
11
  function sleep(ms) {
11
12
  return new Promise((r) => setTimeout(r, ms));
12
13
  }
@@ -138,7 +139,14 @@ class Http {
138
139
  return json.length > 500 ? json.slice(0, 500) + `...(total ${json.length})` : json;
139
140
  }
140
141
  static _pushOutgoingLog(item) {
141
- const entry = { id: this._nextOutgoingLogID++, ...item };
142
+ const entry = {
143
+ id: this._nextOutgoingLogID++,
144
+ ...item,
145
+ url: typeof item.url === "string" ? LogService_1.LogService.sanitize(item.url) : item.url,
146
+ request: typeof item.request === "string" ? LogService_1.LogService.sanitize(item.request) : LogService_1.LogService.sanitizeDeep(item.request),
147
+ response: typeof item.response === "string" ? LogService_1.LogService.sanitize(item.response) : LogService_1.LogService.sanitizeDeep(item.response),
148
+ error: typeof item.error === "string" ? LogService_1.LogService.sanitize(item.error) : item.error,
149
+ };
142
150
  this._outgoingLog.push(entry);
143
151
  if (this._outgoingLog.length > this._outgoingLogLimit)
144
152
  this._outgoingLog.shift();
@@ -0,0 +1,34 @@
1
+ export declare const LOG_SOURCES: readonly ["process-out", "process-err", "nginx-access", "nginx-error"];
2
+ export type TLogSourceId = typeof LOG_SOURCES[number];
3
+ export interface ILogFilesOptions {
4
+ processOut?: string;
5
+ processErr?: string;
6
+ nginxAccess?: string;
7
+ nginxError?: string;
8
+ }
9
+ export interface ILogTailRequest {
10
+ lines?: number;
11
+ since?: number;
12
+ filter?: string;
13
+ }
14
+ export interface ILogTailResult {
15
+ source: TLogSourceId;
16
+ path: string;
17
+ size: number;
18
+ cursor: number;
19
+ rotated: boolean;
20
+ truncated: boolean;
21
+ lines: string[];
22
+ }
23
+ export declare function listLogSources(opts?: ILogFilesOptions): Promise<{
24
+ id: "nginx-access" | "nginx-error" | "process-err" | "process-out";
25
+ label: string;
26
+ path: string | null;
27
+ configured: boolean;
28
+ exists: boolean;
29
+ readable: boolean;
30
+ size: number | null;
31
+ modifiedAt: number | null;
32
+ reason: string | null;
33
+ }[]>;
34
+ export declare function tailLog(id: TLogSourceId, opts: ILogFilesOptions | undefined, req?: ILogTailRequest): Promise<ILogTailResult>;