xtra-cli 1.0.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.
- package/LICENSE +21 -0
- package/README.md +87 -0
- package/dist/bin/xtra.js +138 -0
- package/dist/commands/access.js +184 -0
- package/dist/commands/admin.js +118 -0
- package/dist/commands/audit.js +67 -0
- package/dist/commands/branch.js +212 -0
- package/dist/commands/checkout.js +73 -0
- package/dist/commands/ci.js +341 -0
- package/dist/commands/completion.js +227 -0
- package/dist/commands/diff.js +162 -0
- package/dist/commands/doctor.js +164 -0
- package/dist/commands/env.js +70 -0
- package/dist/commands/export.js +83 -0
- package/dist/commands/generate.js +179 -0
- package/dist/commands/history.js +77 -0
- package/dist/commands/import.js +121 -0
- package/dist/commands/init.js +205 -0
- package/dist/commands/integration.js +188 -0
- package/dist/commands/local.js +198 -0
- package/dist/commands/login.js +198 -0
- package/dist/commands/login.test.js +51 -0
- package/dist/commands/logs.js +121 -0
- package/dist/commands/profile.js +184 -0
- package/dist/commands/project.js +165 -0
- package/dist/commands/rollback.js +95 -0
- package/dist/commands/rotate.js +93 -0
- package/dist/commands/run.js +215 -0
- package/dist/commands/scan.js +127 -0
- package/dist/commands/secrets.js +305 -0
- package/dist/commands/simulate.js +109 -0
- package/dist/commands/status.js +93 -0
- package/dist/commands/template.js +276 -0
- package/dist/commands/ui.js +289 -0
- package/dist/commands/watch.js +123 -0
- package/dist/lib/api.js +187 -0
- package/dist/lib/api.test.js +89 -0
- package/dist/lib/audit.js +136 -0
- package/dist/lib/config.js +70 -0
- package/dist/lib/config.test.js +47 -0
- package/dist/lib/crypto.js +50 -0
- package/dist/lib/manifest.js +52 -0
- package/dist/lib/profiles.js +103 -0
- package/package.json +67 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.uiCommand = void 0;
|
|
40
|
+
const commander_1 = require("commander");
|
|
41
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
42
|
+
const readline = __importStar(require("readline"));
|
|
43
|
+
const api_1 = require("../lib/api");
|
|
44
|
+
const config_1 = require("../lib/config");
|
|
45
|
+
const ENVS = ["development", "staging", "production"];
|
|
46
|
+
const ENV_COLORS = {
|
|
47
|
+
development: chalk_1.default.green,
|
|
48
|
+
staging: chalk_1.default.yellow,
|
|
49
|
+
production: chalk_1.default.red,
|
|
50
|
+
};
|
|
51
|
+
const W = process.stdout.columns || 120;
|
|
52
|
+
// ─── Box Drawing Helpers ─────────────────────────────────────────────────────
|
|
53
|
+
const B = {
|
|
54
|
+
tl: "╭", tr: "╮", bl: "╰", br: "╯",
|
|
55
|
+
h: "─", v: "│", x: "┼",
|
|
56
|
+
lt: "├", rt: "┤", tt: "┬", bt: "┴",
|
|
57
|
+
};
|
|
58
|
+
function hline(width, c = B.h) { return c.repeat(width); }
|
|
59
|
+
function box(title, lines, width, active) {
|
|
60
|
+
const border = active ? chalk_1.default.cyan : chalk_1.default.hex("#2d3a4d");
|
|
61
|
+
const inner = width - 2;
|
|
62
|
+
const pad = (s) => s.padEnd(inner).slice(0, inner);
|
|
63
|
+
const label = active
|
|
64
|
+
? chalk_1.default.bold.cyan(` ${title} `)
|
|
65
|
+
: chalk_1.default.hex("#4a5568")(` ${title} `);
|
|
66
|
+
const titleLine = border(B.tl) + label + border(hline(inner - title.length - 2)) + border(B.tr);
|
|
67
|
+
const body = lines.map(l => border(B.v) + l.slice(0, inner).padEnd(inner) + border(B.v));
|
|
68
|
+
const foot = border(B.bl) + border(hline(inner)) + border(B.br);
|
|
69
|
+
return [titleLine, ...body, foot];
|
|
70
|
+
}
|
|
71
|
+
function clear() { process.stdout.write("\x1B[H\x1B[2J\x1B[3J"); }
|
|
72
|
+
// ─── Main TUI ────────────────────────────────────────────────────────────────
|
|
73
|
+
async function runUI() {
|
|
74
|
+
let panel = "projects";
|
|
75
|
+
let projects = [];
|
|
76
|
+
let projIdx = 0;
|
|
77
|
+
let envIdx = 0;
|
|
78
|
+
let secrets = [];
|
|
79
|
+
let secIdx = 0;
|
|
80
|
+
let status = "Loading…";
|
|
81
|
+
let loading = false;
|
|
82
|
+
let showValues = false;
|
|
83
|
+
// ── Loaders ────────────────────────────────────────────────────────────────
|
|
84
|
+
async function loadProjects() {
|
|
85
|
+
status = "Loading projects…";
|
|
86
|
+
draw();
|
|
87
|
+
try {
|
|
88
|
+
const raw = await api_1.api.getProjects();
|
|
89
|
+
projects = Array.isArray(raw) ? raw : raw.projects ?? [];
|
|
90
|
+
status = projects.length > 0
|
|
91
|
+
? `${projects.length} project(s) found · ↑↓ navigate · Enter to load secrets`
|
|
92
|
+
: "No projects found. Run `xtra login` first.";
|
|
93
|
+
}
|
|
94
|
+
catch (e) {
|
|
95
|
+
status = chalk_1.default.red(`✗ ${e.message}`);
|
|
96
|
+
}
|
|
97
|
+
draw();
|
|
98
|
+
}
|
|
99
|
+
async function loadSecrets() {
|
|
100
|
+
if (!projects[projIdx])
|
|
101
|
+
return;
|
|
102
|
+
loading = true;
|
|
103
|
+
status = `Fetching secrets for ${chalk_1.default.cyan(projects[projIdx].name)} / ${ENV_COLORS[ENVS[envIdx]](ENVS[envIdx])}…`;
|
|
104
|
+
draw();
|
|
105
|
+
try {
|
|
106
|
+
const rc = (0, config_1.getRcConfig)();
|
|
107
|
+
const branch = rc.branch || "main";
|
|
108
|
+
const raw = await api_1.api.getSecrets(projects[projIdx].id, ENVS[envIdx], branch);
|
|
109
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
110
|
+
secrets = Object.entries(raw).map(([key, value]) => ({
|
|
111
|
+
id: key, key, value: String(value), updatedAt: new Date().toISOString(),
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
else if (Array.isArray(raw)) {
|
|
115
|
+
secrets = raw;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
secrets = [];
|
|
119
|
+
}
|
|
120
|
+
secIdx = 0;
|
|
121
|
+
status = secrets.length > 0
|
|
122
|
+
? `${secrets.length} secret(s) · V toggle values · Tab switch panel · Q quit`
|
|
123
|
+
: "No secrets in this environment.";
|
|
124
|
+
}
|
|
125
|
+
catch (e) {
|
|
126
|
+
secrets = [];
|
|
127
|
+
status = chalk_1.default.red(`✗ ${e.message}`);
|
|
128
|
+
}
|
|
129
|
+
loading = false;
|
|
130
|
+
draw();
|
|
131
|
+
}
|
|
132
|
+
// ── Render ─────────────────────────────────────────────────────────────────
|
|
133
|
+
function draw() {
|
|
134
|
+
clear();
|
|
135
|
+
// ── Header ──────────────────────────────────────────────────────────────
|
|
136
|
+
const appTitle = chalk_1.default.bold.cyan(" 🔒 XtraSecurity") + chalk_1.default.hex("#4a5a6b")(" — Interactive Secrets Shell");
|
|
137
|
+
const env = ENV_COLORS[ENVS[envIdx]] ? ENV_COLORS[ENVS[envIdx]](ENVS[envIdx]) : chalk_1.default.white(ENVS[envIdx]);
|
|
138
|
+
const crumb = projects[projIdx]
|
|
139
|
+
? chalk_1.default.hex("#4a5568")(` ${projects[projIdx].name} `) + chalk_1.default.hex("#4a5568")("/") + chalk_1.default.hex("#4a5568")(` ${ENVS[envIdx]}`)
|
|
140
|
+
: chalk_1.default.hex("#4a5568")(" No project selected");
|
|
141
|
+
console.log();
|
|
142
|
+
console.log(appTitle + chalk_1.default.hex("#2d3a4d")(" │") + crumb);
|
|
143
|
+
console.log(chalk_1.default.hex("#1e293b")(hline(W)));
|
|
144
|
+
console.log();
|
|
145
|
+
// ── Left column: Projects (22 chr) + Env (22 chr) ─────────────────────
|
|
146
|
+
const LEFT_W = 26;
|
|
147
|
+
const RIGHT_W = W - LEFT_W - 3;
|
|
148
|
+
// Projects panel
|
|
149
|
+
const projLines = (projects.length > 0 ? projects : [{ id: "", name: "(loading…)" }]).map((p, i) => {
|
|
150
|
+
const active = i === projIdx;
|
|
151
|
+
const icon = active ? chalk_1.default.cyan("▶ ") : " ";
|
|
152
|
+
const name = active && panel === "projects"
|
|
153
|
+
? chalk_1.default.bold.bgCyan.black(` ${p.name} `.padEnd(LEFT_W - 4))
|
|
154
|
+
: active
|
|
155
|
+
? chalk_1.default.bold.cyan(p.name)
|
|
156
|
+
: chalk_1.default.hex("#6b7c93")(p.name);
|
|
157
|
+
return ` ${icon}${name}`;
|
|
158
|
+
});
|
|
159
|
+
const projBox = box("PROJECTS", projLines, LEFT_W, panel === "projects");
|
|
160
|
+
// Env panel
|
|
161
|
+
const envLines = ENVS.map((e, i) => {
|
|
162
|
+
const active = i === envIdx;
|
|
163
|
+
const icon = active ? chalk_1.default.cyan("▶ ") : " ";
|
|
164
|
+
const col = ENV_COLORS[e] ?? chalk_1.default.white;
|
|
165
|
+
const name = active && panel === "env"
|
|
166
|
+
? chalk_1.default.bold.bgCyan.black(` ${e} `.padEnd(LEFT_W - 4))
|
|
167
|
+
: active
|
|
168
|
+
? col.bold(e)
|
|
169
|
+
: chalk_1.default.hex("#6b7c93")(e);
|
|
170
|
+
return ` ${icon}${name}`;
|
|
171
|
+
});
|
|
172
|
+
const envBox = box("ENVIRONMENT", envLines, LEFT_W, panel === "env");
|
|
173
|
+
// Secrets panel
|
|
174
|
+
const KW = Math.floor((RIGHT_W - 3) * 0.45);
|
|
175
|
+
const VW = Math.floor((RIGHT_W - 3) * 0.35);
|
|
176
|
+
const DW = RIGHT_W - KW - VW - 4;
|
|
177
|
+
const secHeader = chalk_1.default.bold.hex("#64748b")("KEY".padEnd(KW)) + " " + chalk_1.default.bold.hex("#64748b")("VALUE".padEnd(VW)) + " " + chalk_1.default.bold.hex("#64748b")("UPDATED".padEnd(DW));
|
|
178
|
+
const divider = chalk_1.default.hex("#1e293b")(hline(KW) + " " + hline(VW) + " " + hline(DW));
|
|
179
|
+
let secLines;
|
|
180
|
+
if (loading) {
|
|
181
|
+
secLines = [` ${chalk_1.default.cyan("⠋")} Loading…`];
|
|
182
|
+
}
|
|
183
|
+
else if (secrets.length === 0) {
|
|
184
|
+
secLines = [` ${chalk_1.default.hex("#4a5568")("No secrets in this environment.")}`];
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
secLines = [" " + secHeader, " " + divider, ...secrets.map((s, i) => {
|
|
188
|
+
const sel = i === secIdx && panel === "secrets";
|
|
189
|
+
const key = s.key.padEnd(KW).slice(0, KW);
|
|
190
|
+
const val = showValues ? s.value.padEnd(VW).slice(0, VW) : "●".repeat(Math.min(8, VW)).padEnd(VW).slice(0, VW);
|
|
191
|
+
const dt = new Date(s.updatedAt).toLocaleDateString("en-GB", { day: "2-digit", month: "short" }).padEnd(DW);
|
|
192
|
+
if (sel) {
|
|
193
|
+
return chalk_1.default.bold.bgHex("#0e243e")(" ▶ " + chalk_1.default.cyan(key) + " " + chalk_1.default.greenBright(val) + " " + chalk_1.default.hex("#4a9eff")(dt) + " ");
|
|
194
|
+
}
|
|
195
|
+
return " " + chalk_1.default.white(key) + " " + chalk_1.default.hex("#4a5568")(val) + " " + chalk_1.default.hex("#374151")(dt);
|
|
196
|
+
})];
|
|
197
|
+
}
|
|
198
|
+
const secBox = box(`SECRETS (${secrets.length})`, secLines, RIGHT_W, panel === "secrets");
|
|
199
|
+
// ── Print side-by-side ────────────────────────────────────────────────
|
|
200
|
+
const leftPanel = [...projBox, ...["", ""], ...envBox];
|
|
201
|
+
const maxRows = Math.max(leftPanel.length, secBox.length);
|
|
202
|
+
const gap = chalk_1.default.hex("#1e293b")(" │ ");
|
|
203
|
+
for (let r = 0; r < maxRows; r++) {
|
|
204
|
+
const l = leftPanel[r] ?? " ".repeat(LEFT_W);
|
|
205
|
+
const rr = secBox[r] ?? " ".repeat(RIGHT_W);
|
|
206
|
+
console.log(l + gap + rr);
|
|
207
|
+
}
|
|
208
|
+
// ── Footer ───────────────────────────────────────────────────────────
|
|
209
|
+
console.log();
|
|
210
|
+
console.log(chalk_1.default.hex("#1e293b")(hline(W)));
|
|
211
|
+
const keys = [
|
|
212
|
+
[chalk_1.default.hex("#64748b")("↑↓"), "Navigate"],
|
|
213
|
+
[chalk_1.default.hex("#64748b")("Enter"), "Select"],
|
|
214
|
+
[chalk_1.default.hex("#64748b")("Tab"), "Switch Panel"],
|
|
215
|
+
[chalk_1.default.hex("#64748b")("V"), showValues ? chalk_1.default.green("Hide values") : chalk_1.default.yellow("Show values")],
|
|
216
|
+
[chalk_1.default.hex("#64748b")("Q"), "Quit"],
|
|
217
|
+
].map(([k, a]) => `${k} ${chalk_1.default.hex("#374151")(a)}`).join(chalk_1.default.hex("#1e293b")(" · "));
|
|
218
|
+
console.log(" " + keys);
|
|
219
|
+
console.log();
|
|
220
|
+
console.log(" " + (status || ""));
|
|
221
|
+
}
|
|
222
|
+
// ── Keyboard ──────────────────────────────────────────────────────────────
|
|
223
|
+
readline.emitKeypressEvents(process.stdin);
|
|
224
|
+
if (process.stdin.isTTY)
|
|
225
|
+
process.stdin.setRawMode(true);
|
|
226
|
+
process.stdin.on("keypress", async (_str, key) => {
|
|
227
|
+
if (!key)
|
|
228
|
+
return;
|
|
229
|
+
// Quit
|
|
230
|
+
if (key.name === "q" || (key.ctrl && key.name === "c")) {
|
|
231
|
+
if (process.stdin.isTTY)
|
|
232
|
+
process.stdin.setRawMode(false);
|
|
233
|
+
clear();
|
|
234
|
+
console.log(chalk_1.default.cyan(" Goodbye! 👋\n"));
|
|
235
|
+
process.exit(0);
|
|
236
|
+
}
|
|
237
|
+
// Toggle secret values with V
|
|
238
|
+
if (key.name === "v") {
|
|
239
|
+
showValues = !showValues;
|
|
240
|
+
draw();
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
// Tab cycle panels
|
|
244
|
+
if (key.name === "tab") {
|
|
245
|
+
panel = panel === "projects" ? "env" : panel === "env" ? "secrets" : "projects";
|
|
246
|
+
draw();
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (panel === "projects") {
|
|
250
|
+
if (key.name === "up")
|
|
251
|
+
projIdx = Math.max(0, projIdx - 1);
|
|
252
|
+
if (key.name === "down")
|
|
253
|
+
projIdx = Math.min(projects.length - 1, projIdx + 1);
|
|
254
|
+
if (key.name === "return") {
|
|
255
|
+
panel = "env";
|
|
256
|
+
draw();
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (panel === "env") {
|
|
261
|
+
if (key.name === "up")
|
|
262
|
+
envIdx = Math.max(0, envIdx - 1);
|
|
263
|
+
if (key.name === "down")
|
|
264
|
+
envIdx = Math.min(ENVS.length - 1, envIdx + 1);
|
|
265
|
+
if (key.name === "return") {
|
|
266
|
+
panel = "secrets";
|
|
267
|
+
await loadSecrets();
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (panel === "secrets") {
|
|
272
|
+
if (key.name === "up")
|
|
273
|
+
secIdx = Math.max(0, secIdx - 1);
|
|
274
|
+
if (key.name === "down")
|
|
275
|
+
secIdx = Math.min(secrets.length - 1, secIdx + 1);
|
|
276
|
+
}
|
|
277
|
+
draw();
|
|
278
|
+
});
|
|
279
|
+
// ── Boot ──────────────────────────────────────────────────────────────────
|
|
280
|
+
await loadProjects();
|
|
281
|
+
if (projects.length > 0)
|
|
282
|
+
await loadSecrets();
|
|
283
|
+
else
|
|
284
|
+
draw();
|
|
285
|
+
}
|
|
286
|
+
// ─── Commander ───────────────────────────────────────────────────────────────
|
|
287
|
+
exports.uiCommand = new commander_1.Command("ui")
|
|
288
|
+
.description("Launch interactive TUI secrets dashboard (arrow keys, Tab, Q to quit)")
|
|
289
|
+
.action(async () => { await runUI(); });
|
|
@@ -0,0 +1,123 @@
|
|
|
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.watchCommand = void 0;
|
|
7
|
+
/**
|
|
8
|
+
* watch.ts — Live reload secrets in dev mode
|
|
9
|
+
*
|
|
10
|
+
* Polls XtraSecurity Cloud for secret changes at a configurable interval.
|
|
11
|
+
* When a change is detected, restarts the child process with fresh secrets.
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* xtra watch -p proj123 -e dev node app.js
|
|
15
|
+
* xtra watch -p proj123 -e dev --interval 10 npm run dev --shell
|
|
16
|
+
*/
|
|
17
|
+
const commander_1 = require("commander");
|
|
18
|
+
const api_1 = require("../lib/api");
|
|
19
|
+
const config_1 = require("../lib/config");
|
|
20
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
21
|
+
const child_process_1 = require("child_process");
|
|
22
|
+
let child = null;
|
|
23
|
+
let lastHash = "";
|
|
24
|
+
function hashSecrets(secrets) {
|
|
25
|
+
return JSON.stringify(Object.keys(secrets).sort().map(k => `${k}=${secrets[k]}`));
|
|
26
|
+
}
|
|
27
|
+
async function startProcess(command, args, secrets, useShell) {
|
|
28
|
+
if (child) {
|
|
29
|
+
process.stdout.write(chalk_1.default.cyan("\n [watch] ") + chalk_1.default.yellow("Secret change detected — restarting...\n"));
|
|
30
|
+
child.kill("SIGTERM");
|
|
31
|
+
// Give it 500ms to terminate gracefully
|
|
32
|
+
await new Promise(r => setTimeout(r, 500));
|
|
33
|
+
}
|
|
34
|
+
const envVars = { ...process.env, ...secrets };
|
|
35
|
+
process.stdout.write(chalk_1.default.cyan(" [watch] ") + chalk_1.default.green(`Starting: ${command} ${args.join(" ")}\n`));
|
|
36
|
+
child = (0, child_process_1.spawn)(command, args, {
|
|
37
|
+
env: envVars,
|
|
38
|
+
stdio: "inherit",
|
|
39
|
+
shell: useShell,
|
|
40
|
+
});
|
|
41
|
+
child.on("exit", (code) => {
|
|
42
|
+
if (code !== null && code !== 0) {
|
|
43
|
+
process.stdout.write(chalk_1.default.cyan(" [watch] ") + chalk_1.default.red(`Process exited with code ${code}\n`));
|
|
44
|
+
}
|
|
45
|
+
child = null;
|
|
46
|
+
});
|
|
47
|
+
child.on("error", (err) => {
|
|
48
|
+
process.stdout.write(chalk_1.default.cyan(" [watch] ") + chalk_1.default.red(`Failed to start: ${err.message}\n`));
|
|
49
|
+
child = null;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
exports.watchCommand = new commander_1.Command("watch")
|
|
53
|
+
.description("Live reload — auto-restart process when secrets change in cloud")
|
|
54
|
+
.option("-p, --project <id>", "Project ID")
|
|
55
|
+
.option("-e, --env <environment>", "Environment", "development")
|
|
56
|
+
.option("-b, --branch <branch>", "Branch", "main")
|
|
57
|
+
.option("--interval <seconds>", "Poll interval in seconds", "5")
|
|
58
|
+
.option("--shell", "Use shell mode for the child process (for npm run etc.)", false)
|
|
59
|
+
.argument("<command>", "Command to run")
|
|
60
|
+
.argument("[args...]", "Command arguments")
|
|
61
|
+
.addHelpText("after", `
|
|
62
|
+
Examples:
|
|
63
|
+
$ xtra watch -p proj123 -e dev node app.js
|
|
64
|
+
$ xtra watch -p proj123 -e dev --interval 10 --shell npm run dev
|
|
65
|
+
$ xtra watch -p proj123 -e dev -- node -r dotenv/config server.js
|
|
66
|
+
`)
|
|
67
|
+
.action(async (command, args, options) => {
|
|
68
|
+
let { project, env, branch, interval, shell: useShell } = options;
|
|
69
|
+
const envMap = { dev: "development", stg: "staging", prod: "production" };
|
|
70
|
+
env = envMap[env] || env;
|
|
71
|
+
if (!project)
|
|
72
|
+
project = (0, config_1.getRcConfig)().project;
|
|
73
|
+
if (!project) {
|
|
74
|
+
console.error(chalk_1.default.red("Error: Project ID required. Use -p <id> or run 'xtra project set'."));
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
// Block production watch — too dangerous
|
|
78
|
+
if (env === "production") {
|
|
79
|
+
console.error(chalk_1.default.red("⚠ xtra watch is not allowed in PRODUCTION for safety reasons."));
|
|
80
|
+
console.error(chalk_1.default.gray(" Use 'xtra run' for one-shot production injection."));
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
const pollMs = Math.max(3, parseInt(interval)) * 1000;
|
|
84
|
+
const isWindows = process.platform === "win32";
|
|
85
|
+
const finalShell = useShell || isWindows;
|
|
86
|
+
console.log(chalk_1.default.bold.cyan("\n● xtra watch") + chalk_1.default.bold(` — watching ${env}/${branch} (every ${interval}s)\n`));
|
|
87
|
+
console.log(chalk_1.default.gray(` Press Ctrl+C to stop.\n`));
|
|
88
|
+
// Graceful shutdown
|
|
89
|
+
process.on("SIGINT", () => {
|
|
90
|
+
if (child)
|
|
91
|
+
child.kill("SIGTERM");
|
|
92
|
+
console.log(chalk_1.default.cyan("\n [watch] ") + chalk_1.default.gray("Stopped."));
|
|
93
|
+
process.exit(0);
|
|
94
|
+
});
|
|
95
|
+
// Initial fetch & start
|
|
96
|
+
try {
|
|
97
|
+
const secrets = await api_1.api.getSecrets(project, env, branch);
|
|
98
|
+
lastHash = hashSecrets(secrets);
|
|
99
|
+
await startProcess(command, args, secrets, finalShell);
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
console.error(chalk_1.default.red("Failed to fetch secrets: " + (e?.response?.data?.error || e.message)));
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
// Poll loop
|
|
106
|
+
setInterval(async () => {
|
|
107
|
+
try {
|
|
108
|
+
const secrets = await api_1.api.getSecrets(project, env, branch);
|
|
109
|
+
const hash = hashSecrets(secrets);
|
|
110
|
+
if (hash !== lastHash) {
|
|
111
|
+
lastHash = hash;
|
|
112
|
+
await startProcess(command, args, secrets, finalShell);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
process.stdout.write(chalk_1.default.cyan(" [watch] ") + chalk_1.default.gray(`${new Date().toLocaleTimeString()} — no changes \r`));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch (e) {
|
|
119
|
+
process.stdout.write(chalk_1.default.cyan("\n [watch] ") + chalk_1.default.yellow(`Poll failed: ${e.message}\n`));
|
|
120
|
+
// Don't exit — keep trying
|
|
121
|
+
}
|
|
122
|
+
}, pollMs);
|
|
123
|
+
});
|
package/dist/lib/api.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
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.api = void 0;
|
|
7
|
+
const axios_1 = __importDefault(require("axios"));
|
|
8
|
+
const config_1 = require("./config");
|
|
9
|
+
const getClient = () => {
|
|
10
|
+
const { apiUrl } = (0, config_1.getConfig)();
|
|
11
|
+
const token = (0, config_1.getAuthToken)();
|
|
12
|
+
const client = axios_1.default.create({
|
|
13
|
+
baseURL: apiUrl,
|
|
14
|
+
headers: {
|
|
15
|
+
"Content-Type": "application/json",
|
|
16
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
return client;
|
|
20
|
+
};
|
|
21
|
+
exports.api = {
|
|
22
|
+
login: async (email, password, apiKey) => {
|
|
23
|
+
const response = await getClient().post("/auth/cli-login", {
|
|
24
|
+
email,
|
|
25
|
+
password,
|
|
26
|
+
apiKey,
|
|
27
|
+
});
|
|
28
|
+
return response.data;
|
|
29
|
+
},
|
|
30
|
+
// Placeholders for future methods
|
|
31
|
+
getSecrets: async (projectId, env, branch = "main") => {
|
|
32
|
+
// Fixed path to match Next.js dynamic route folder structure
|
|
33
|
+
const response = await getClient().get(`/projects/${projectId}/envs/${env}/secrets?branch=${branch}`);
|
|
34
|
+
return response.data;
|
|
35
|
+
},
|
|
36
|
+
getSecretVersions: async (projectId, env, branch = "main") => {
|
|
37
|
+
const response = await getClient().get(`/projects/${projectId}/envs/${env}/secrets?includeVersions=true&branch=${branch}`);
|
|
38
|
+
return response.data;
|
|
39
|
+
},
|
|
40
|
+
setSecrets: async (projectId, env, secrets, expectedVersions, branch = "main") => {
|
|
41
|
+
const response = await getClient().post(`/projects/${projectId}/envs/${env}/secrets`, { secrets, expectedVersions, branch });
|
|
42
|
+
return response.data;
|
|
43
|
+
},
|
|
44
|
+
getSecretDetails: async (projectId, env, key, branch = "main") => {
|
|
45
|
+
const response = await getClient().get(`/projects/${projectId}/envs/${env}/secrets/${key}?branch=${branch}`);
|
|
46
|
+
return response.data;
|
|
47
|
+
},
|
|
48
|
+
syncLogs: async (logs) => {
|
|
49
|
+
const response = await getClient().post("/audit/cli-logs", { logs });
|
|
50
|
+
return response.data;
|
|
51
|
+
},
|
|
52
|
+
linkSecret: async (projectId, env, key, sourceProjectId, sourceEnv, sourceKey) => {
|
|
53
|
+
const response = await getClient().post(`/projects/${projectId}/envs/${env}/secrets/link`, {
|
|
54
|
+
key,
|
|
55
|
+
sourceProjectId,
|
|
56
|
+
sourceEnv,
|
|
57
|
+
sourceKey
|
|
58
|
+
});
|
|
59
|
+
return response.data;
|
|
60
|
+
},
|
|
61
|
+
rotateSecret: async (projectId, env, key, strategy, parsedNewValue) => {
|
|
62
|
+
const response = await getClient().post(`/projects/${projectId}/envs/${env}/secrets/${key}/rotate`, {
|
|
63
|
+
strategy,
|
|
64
|
+
parsedNewValue
|
|
65
|
+
});
|
|
66
|
+
return response.data;
|
|
67
|
+
},
|
|
68
|
+
promoteSecret: async (projectId, env, key) => {
|
|
69
|
+
const response = await getClient().post(`/projects/${projectId}/envs/${env}/secrets/${key}/promote`, {});
|
|
70
|
+
return response.data;
|
|
71
|
+
},
|
|
72
|
+
verifyAuditLogs: async () => {
|
|
73
|
+
const response = await getClient().get("/audit/verify");
|
|
74
|
+
return response.data;
|
|
75
|
+
},
|
|
76
|
+
exportAuditLogs: async (format, start, end, projectId) => {
|
|
77
|
+
const body = { format };
|
|
78
|
+
if (start)
|
|
79
|
+
body.startDate = start;
|
|
80
|
+
if (end)
|
|
81
|
+
body.endDate = end;
|
|
82
|
+
if (projectId)
|
|
83
|
+
body.projectId = projectId;
|
|
84
|
+
const response = await getClient().post("/audit/export", body, { responseType: format === "csv" ? "text" : "json" });
|
|
85
|
+
return response.data;
|
|
86
|
+
},
|
|
87
|
+
// JIT Access
|
|
88
|
+
requestAccess: async (projectId, reason, duration, secretId) => {
|
|
89
|
+
const response = await getClient().post("/access/request", { projectId, secretId, reason, duration });
|
|
90
|
+
return response.data;
|
|
91
|
+
},
|
|
92
|
+
approveAccess: async (requestId, decision) => {
|
|
93
|
+
const response = await getClient().post("/access/approve", { requestId, decision });
|
|
94
|
+
return response.data;
|
|
95
|
+
},
|
|
96
|
+
listAccessRequests: async (mode) => {
|
|
97
|
+
const response = await getClient().get(`/access/list?mode=${mode}`);
|
|
98
|
+
return response.data;
|
|
99
|
+
},
|
|
100
|
+
// Project Management
|
|
101
|
+
getProjects: async () => {
|
|
102
|
+
const { workspace } = (0, config_1.getConfig)();
|
|
103
|
+
const url = workspace ? `/project?workspaceId=${workspace}` : "/project";
|
|
104
|
+
const response = await getClient().get(url);
|
|
105
|
+
return response.data;
|
|
106
|
+
},
|
|
107
|
+
// Branch Management
|
|
108
|
+
getBranches: async (projectId) => {
|
|
109
|
+
const response = await getClient().get(`/branch?projectId=${projectId}`);
|
|
110
|
+
return response.data;
|
|
111
|
+
},
|
|
112
|
+
createBranch: async (projectId, name, description) => {
|
|
113
|
+
const response = await getClient().post("/branch", { projectId, name, description });
|
|
114
|
+
return response.data;
|
|
115
|
+
},
|
|
116
|
+
deleteBranch: async (branchId) => {
|
|
117
|
+
const response = await getClient().delete(`/branch/${branchId}`);
|
|
118
|
+
return response.data;
|
|
119
|
+
},
|
|
120
|
+
updateBranch: async (branchId, updates) => {
|
|
121
|
+
const response = await getClient().put("/branch", { id: branchId, ...updates });
|
|
122
|
+
return response.data;
|
|
123
|
+
},
|
|
124
|
+
// Admin - Role Management
|
|
125
|
+
getRoles: async () => {
|
|
126
|
+
const response = await getClient().get("/admin/roles");
|
|
127
|
+
return response.data;
|
|
128
|
+
},
|
|
129
|
+
getUsers: async (teamId) => {
|
|
130
|
+
const params = teamId ? `?teamId=${teamId}` : "";
|
|
131
|
+
const response = await getClient().get(`/admin/users${params}`);
|
|
132
|
+
return response.data;
|
|
133
|
+
},
|
|
134
|
+
setUserRole: async (email, role, teamId) => {
|
|
135
|
+
const response = await getClient().put("/admin/users/role", { email, role, teamId });
|
|
136
|
+
return response.data;
|
|
137
|
+
},
|
|
138
|
+
// Integrations
|
|
139
|
+
getIntegrationStatus: async (provider) => {
|
|
140
|
+
const response = await getClient().get(`/integrations/${provider}`);
|
|
141
|
+
return response.data;
|
|
142
|
+
},
|
|
143
|
+
getIntegrationRepos: async (provider) => {
|
|
144
|
+
const response = await getClient().get(`/integrations/${provider}/sync`);
|
|
145
|
+
return response.data.repos;
|
|
146
|
+
},
|
|
147
|
+
syncSecretsToGithub: async (data) => {
|
|
148
|
+
const response = await getClient().post("/api/integrations/github/sync", data);
|
|
149
|
+
return response.data;
|
|
150
|
+
},
|
|
151
|
+
exportKubernetesSecret: async (projectId, params) => {
|
|
152
|
+
const query = new URLSearchParams(params).toString();
|
|
153
|
+
const response = await getClient().get(`/projects/${projectId}/kubernetes?${query}`);
|
|
154
|
+
return response.data; // This returns the YAML string directly
|
|
155
|
+
},
|
|
156
|
+
// Advanced Features
|
|
157
|
+
getSecretHistory: async (projectId, env, key) => {
|
|
158
|
+
const response = await getClient().get(`/projects/${projectId}/envs/${env}/secrets/${key}/history`);
|
|
159
|
+
return response.data;
|
|
160
|
+
},
|
|
161
|
+
rollbackSecret: async (projectId, env, key, version) => {
|
|
162
|
+
const response = await getClient().post(`/projects/${projectId}/envs/${env}/secrets/${key}/history`, { version });
|
|
163
|
+
return response.data;
|
|
164
|
+
},
|
|
165
|
+
cloneEnvironment: async (projectId, fromEnv, toEnv, overwrite, branch) => {
|
|
166
|
+
const response = await getClient().post(`/projects/${projectId}/envs/clone`, { fromEnv, toEnv, overwrite, branch });
|
|
167
|
+
return response.data;
|
|
168
|
+
},
|
|
169
|
+
// JIT Link Management
|
|
170
|
+
generateJitLink: async (opts) => {
|
|
171
|
+
const response = await getClient().post("/jit/generate", opts);
|
|
172
|
+
return response.data;
|
|
173
|
+
},
|
|
174
|
+
claimJitLink: async (token) => {
|
|
175
|
+
const response = await getClient().post("/jit/claim", { token });
|
|
176
|
+
return response.data;
|
|
177
|
+
},
|
|
178
|
+
getJitInfo: async (token) => {
|
|
179
|
+
const response = await getClient().get(`/jit/${token}`);
|
|
180
|
+
return response.data;
|
|
181
|
+
},
|
|
182
|
+
// Generic POST method for audit logging and other uses
|
|
183
|
+
post: async (endpoint, data) => {
|
|
184
|
+
const response = await getClient().post(endpoint, data);
|
|
185
|
+
return response.data;
|
|
186
|
+
}
|
|
187
|
+
};
|