@tenonhq/sincronia-core 0.0.67 → 0.0.69
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/dist/FileUtils.js +60 -0
- package/dist/appUtils.js +13 -10
- package/dist/clickupCommands.js +2 -59
- package/dist/commander.js +29 -1
- package/dist/commands.js +2 -2
- package/dist/config.js +9 -0
- package/dist/initSystem/corePlugin.js +210 -0
- package/dist/initSystem/discovery.js +91 -0
- package/dist/initSystem/orchestrator.js +312 -0
- package/dist/loginCommand.js +20 -0
- package/dist/tests/clickupCommands.test.js +9 -1
- package/dist/tests/discovery.test.js +56 -0
- package/dist/tests/orchestrator.test.js +23 -0
- package/dist/tests/writeEnvVar.test.js +119 -0
- package/dist/wizard.js +8 -146
- package/package.json +2 -4
- package/LICENSE +0 -674
|
@@ -0,0 +1,312 @@
|
|
|
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.runInit = runInit;
|
|
7
|
+
exports.runLogin = runLogin;
|
|
8
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
9
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
10
|
+
const dotenv_1 = __importDefault(require("dotenv"));
|
|
11
|
+
const fs_1 = __importDefault(require("fs"));
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
const Logger_1 = require("../Logger");
|
|
14
|
+
const FileUtils_1 = require("../FileUtils");
|
|
15
|
+
const discovery_1 = require("./discovery");
|
|
16
|
+
const corePlugin_1 = require("./corePlugin");
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Init Context
|
|
19
|
+
// ============================================================================
|
|
20
|
+
/**
|
|
21
|
+
* @description Builds an InitContext from the current environment.
|
|
22
|
+
* Reads existing .env if present. Pulls process.env fallbacks only for
|
|
23
|
+
* env keys declared by the provided plugins (no hardcoded allowlist).
|
|
24
|
+
*/
|
|
25
|
+
function buildInitContext(plugins) {
|
|
26
|
+
const rootDir = process.cwd();
|
|
27
|
+
const envPath = path_1.default.resolve(rootDir, ".env");
|
|
28
|
+
const env = {};
|
|
29
|
+
// Load existing .env values
|
|
30
|
+
try {
|
|
31
|
+
const parsed = dotenv_1.default.parse(fs_1.default.readFileSync(envPath, "utf8"));
|
|
32
|
+
Object.assign(env, parsed);
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
// No .env yet — starting fresh
|
|
36
|
+
}
|
|
37
|
+
// Pull from process.env for keys declared by plugins but missing from .env
|
|
38
|
+
const pluginEnvKeys = plugins.flatMap(p => [
|
|
39
|
+
...(p.login || []).map(h => h.envKey),
|
|
40
|
+
...(p.configure || []).map(h => h.key),
|
|
41
|
+
]);
|
|
42
|
+
pluginEnvKeys.forEach(key => {
|
|
43
|
+
if (process.env[key] && !env[key]) {
|
|
44
|
+
env[key] = process.env[key];
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
let hasConfig = false;
|
|
48
|
+
try {
|
|
49
|
+
fs_1.default.accessSync(path_1.default.join(rootDir, "sinc.config.js"), fs_1.default.constants.F_OK);
|
|
50
|
+
hasConfig = true;
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
// No config yet
|
|
54
|
+
}
|
|
55
|
+
return { env, answers: {}, rootDir, hasConfig, inquirer: inquirer_1.default, chalk: chalk_1.default };
|
|
56
|
+
}
|
|
57
|
+
// ============================================================================
|
|
58
|
+
// Plugin Selection
|
|
59
|
+
// ============================================================================
|
|
60
|
+
async function promptPluginSelection(externalPlugins) {
|
|
61
|
+
if (externalPlugins.length === 0) {
|
|
62
|
+
return [corePlugin_1.corePlugin];
|
|
63
|
+
}
|
|
64
|
+
const choices = externalPlugins.map(plugin => ({
|
|
65
|
+
name: plugin.displayName + " — " + plugin.description,
|
|
66
|
+
value: plugin.name,
|
|
67
|
+
checked: false,
|
|
68
|
+
}));
|
|
69
|
+
const answer = await inquirer_1.default.prompt([{
|
|
70
|
+
type: "checkbox",
|
|
71
|
+
name: "plugins",
|
|
72
|
+
message: "Which integrations would you like to configure?",
|
|
73
|
+
choices,
|
|
74
|
+
}]);
|
|
75
|
+
const selectedNames = new Set(answer.plugins);
|
|
76
|
+
const selected = externalPlugins.filter(p => selectedNames.has(p.name));
|
|
77
|
+
return [corePlugin_1.corePlugin, ...selected];
|
|
78
|
+
}
|
|
79
|
+
async function runLoginPhase(plugin, context) {
|
|
80
|
+
const hooks = plugin.login;
|
|
81
|
+
if (!hooks || hooks.length === 0)
|
|
82
|
+
return;
|
|
83
|
+
for (const hook of hooks) {
|
|
84
|
+
const existingValue = context.env[hook.envKey] || "";
|
|
85
|
+
// Show instructions if provided
|
|
86
|
+
if (hook.instructions && hook.instructions.length > 0) {
|
|
87
|
+
Logger_1.logger.info("");
|
|
88
|
+
hook.instructions.forEach(line => Logger_1.logger.info(line));
|
|
89
|
+
Logger_1.logger.info("");
|
|
90
|
+
}
|
|
91
|
+
// Build the prompt
|
|
92
|
+
const promptConfig = {
|
|
93
|
+
type: hook.prompt.type,
|
|
94
|
+
name: "value",
|
|
95
|
+
message: hook.prompt.message,
|
|
96
|
+
};
|
|
97
|
+
if (hook.prompt.mask) {
|
|
98
|
+
promptConfig.mask = hook.prompt.mask;
|
|
99
|
+
}
|
|
100
|
+
// Show existing value as default for non-password fields
|
|
101
|
+
if (existingValue && hook.prompt.type !== "password") {
|
|
102
|
+
promptConfig.default = existingValue;
|
|
103
|
+
}
|
|
104
|
+
// Add basic validation for required fields
|
|
105
|
+
if (hook.required !== false) {
|
|
106
|
+
promptConfig.validate = (input) => {
|
|
107
|
+
if (!input || input.trim() === "")
|
|
108
|
+
return "This field is required";
|
|
109
|
+
return true;
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const answer = await inquirer_1.default.prompt([promptConfig]);
|
|
113
|
+
context.env[hook.envKey] = answer.value.trim();
|
|
114
|
+
}
|
|
115
|
+
// Run per-hook validation if defined
|
|
116
|
+
for (const hook of hooks) {
|
|
117
|
+
if (hook.validate) {
|
|
118
|
+
const result = await hook.validate(context.env[hook.envKey], context);
|
|
119
|
+
if (result !== true) {
|
|
120
|
+
Logger_1.logger.error(chalk_1.default.red("✗ " + result));
|
|
121
|
+
throw new Error("Validation failed for " + hook.envKey);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// Core plugin gets special post-login validation (all 3 credentials at once)
|
|
126
|
+
if (plugin.name === "core") {
|
|
127
|
+
Logger_1.logger.info("Validating credentials...");
|
|
128
|
+
const coreResult = await (0, corePlugin_1.validateCoreLogin)(context);
|
|
129
|
+
if (coreResult !== true) {
|
|
130
|
+
Logger_1.logger.error(chalk_1.default.red("✗ " + coreResult));
|
|
131
|
+
throw new Error("ServiceNow login failed");
|
|
132
|
+
}
|
|
133
|
+
Logger_1.logger.success(chalk_1.default.green("✓ Connected to " + context.env.SN_INSTANCE));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
async function runConfigurePhase(plugin, context) {
|
|
137
|
+
const hooks = plugin.configure;
|
|
138
|
+
if (!hooks || hooks.length === 0)
|
|
139
|
+
return;
|
|
140
|
+
for (const hook of hooks) {
|
|
141
|
+
Logger_1.logger.debug("Running configure hook: " + hook.label);
|
|
142
|
+
const result = await hook.run(context);
|
|
143
|
+
if (result !== null && result !== undefined) {
|
|
144
|
+
context.answers[hook.key] = result;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* @description Saves env vars declared by plugins to .env (merge-style).
|
|
150
|
+
* Only writes keys that plugins explicitly declared via login hooks or
|
|
151
|
+
* configure hooks — never writes transient context values.
|
|
152
|
+
*/
|
|
153
|
+
function saveEnvVars(context, plugins) {
|
|
154
|
+
const pluginKeys = new Set();
|
|
155
|
+
plugins.forEach(plugin => {
|
|
156
|
+
(plugin.login || []).forEach(hook => pluginKeys.add(hook.envKey));
|
|
157
|
+
(plugin.configure || []).forEach(hook => {
|
|
158
|
+
if (context.env[hook.key])
|
|
159
|
+
pluginKeys.add(hook.key);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
const vars = Array.from(pluginKeys)
|
|
163
|
+
.filter(key => context.env[key])
|
|
164
|
+
.map(key => ({ key, value: context.env[key] }));
|
|
165
|
+
if (vars.length > 0) {
|
|
166
|
+
(0, FileUtils_1.writeEnvVars)({ vars });
|
|
167
|
+
// Also set on process.env for immediate use
|
|
168
|
+
vars.forEach(({ key, value }) => { process.env[key] = value; });
|
|
169
|
+
Logger_1.logger.success(chalk_1.default.green("✓ Saved to .env (existing values preserved)"));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async function runInit(options) {
|
|
173
|
+
let failed = false;
|
|
174
|
+
try {
|
|
175
|
+
Logger_1.logger.info("");
|
|
176
|
+
Logger_1.logger.info(chalk_1.default.bold(" Sincronia Setup"));
|
|
177
|
+
Logger_1.logger.info(" " + "═".repeat(40));
|
|
178
|
+
Logger_1.logger.info("");
|
|
179
|
+
// 1. Discover plugins
|
|
180
|
+
const externalPlugins = (0, discovery_1.discoverPlugins)();
|
|
181
|
+
if (externalPlugins.length > 0) {
|
|
182
|
+
Logger_1.logger.info(" Detected packages:");
|
|
183
|
+
Logger_1.logger.info(" ● sincronia-core (" + chalk_1.default.cyan("ServiceNow") + ")");
|
|
184
|
+
externalPlugins.forEach(p => {
|
|
185
|
+
Logger_1.logger.info(" ● sincronia-" + p.name + " (" + chalk_1.default.cyan(p.displayName) + ")");
|
|
186
|
+
});
|
|
187
|
+
Logger_1.logger.info("");
|
|
188
|
+
}
|
|
189
|
+
// 2. Select plugins
|
|
190
|
+
const selectedPlugins = await promptPluginSelection(externalPlugins);
|
|
191
|
+
// 3. Build context (passes plugins so env fallback uses their declared keys)
|
|
192
|
+
const context = buildInitContext(selectedPlugins);
|
|
193
|
+
// 4. Login phase
|
|
194
|
+
Logger_1.logger.info("");
|
|
195
|
+
Logger_1.logger.info(chalk_1.default.bold(" ── Login " + "─".repeat(30)));
|
|
196
|
+
Logger_1.logger.info("");
|
|
197
|
+
for (const plugin of selectedPlugins) {
|
|
198
|
+
await runLoginPhase(plugin, context);
|
|
199
|
+
}
|
|
200
|
+
// 5. Save env vars after login
|
|
201
|
+
saveEnvVars(context, selectedPlugins);
|
|
202
|
+
// 6. Configure phase
|
|
203
|
+
Logger_1.logger.info("");
|
|
204
|
+
Logger_1.logger.info(chalk_1.default.bold(" ── Configure " + "─".repeat(26)));
|
|
205
|
+
Logger_1.logger.info("");
|
|
206
|
+
for (const plugin of selectedPlugins) {
|
|
207
|
+
await runConfigurePhase(plugin, context);
|
|
208
|
+
}
|
|
209
|
+
// 7. Initialize phase
|
|
210
|
+
Logger_1.logger.info("");
|
|
211
|
+
Logger_1.logger.info(chalk_1.default.bold(" ── Initialize " + "─".repeat(25)));
|
|
212
|
+
Logger_1.logger.info("");
|
|
213
|
+
for (const plugin of selectedPlugins) {
|
|
214
|
+
if (plugin.initialize) {
|
|
215
|
+
try {
|
|
216
|
+
await plugin.initialize(context);
|
|
217
|
+
}
|
|
218
|
+
catch (e) {
|
|
219
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
220
|
+
Logger_1.logger.error("Initialization failed for " + plugin.displayName + ": " + msg);
|
|
221
|
+
failed = true;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// 8. Summary
|
|
226
|
+
Logger_1.logger.info("");
|
|
227
|
+
Logger_1.logger.info(" " + "═".repeat(40));
|
|
228
|
+
if (failed) {
|
|
229
|
+
Logger_1.logger.warn(" Setup completed with errors. Review the output above.");
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
Logger_1.logger.success(chalk_1.default.green(" Setup complete!") + " Run " + chalk_1.default.cyan("sinc watch") + " to start.");
|
|
233
|
+
}
|
|
234
|
+
Logger_1.logger.info("");
|
|
235
|
+
}
|
|
236
|
+
catch (e) {
|
|
237
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
238
|
+
Logger_1.logger.error("Init failed: " + message);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
async function runLogin(options) {
|
|
242
|
+
try {
|
|
243
|
+
const opts = options || {};
|
|
244
|
+
// Determine which plugins to log in to
|
|
245
|
+
let pluginsToLogin;
|
|
246
|
+
const coreAliases = new Set(["core", "servicenow", "sn"]);
|
|
247
|
+
if (opts.pluginName) {
|
|
248
|
+
if (coreAliases.has(opts.pluginName)) {
|
|
249
|
+
pluginsToLogin = [corePlugin_1.corePlugin];
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
const externalPlugins = (0, discovery_1.discoverPlugins)();
|
|
253
|
+
const match = externalPlugins.find(p => p.name === opts.pluginName);
|
|
254
|
+
if (!match) {
|
|
255
|
+
Logger_1.logger.error("Plugin '" + opts.pluginName + "' not found. Available plugins:");
|
|
256
|
+
externalPlugins.forEach(p => Logger_1.logger.info(" - " + p.name + " (" + p.displayName + ")"));
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
pluginsToLogin = [match];
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
else if (opts.all) {
|
|
263
|
+
pluginsToLogin = [corePlugin_1.corePlugin, ...(0, discovery_1.discoverPlugins)()];
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
pluginsToLogin = [corePlugin_1.corePlugin];
|
|
267
|
+
}
|
|
268
|
+
// Build context with discovered plugins for env key resolution
|
|
269
|
+
const context = buildInitContext(pluginsToLogin);
|
|
270
|
+
// Apply CLI flag overrides for non-interactive mode
|
|
271
|
+
if (opts.instance)
|
|
272
|
+
context.env.SN_INSTANCE = opts.instance;
|
|
273
|
+
if (opts.user)
|
|
274
|
+
context.env.SN_USER = opts.user;
|
|
275
|
+
if (opts.password)
|
|
276
|
+
context.env.SN_PASSWORD = opts.password;
|
|
277
|
+
// Dynamic header based on which plugins are being logged in
|
|
278
|
+
const pluginNames = pluginsToLogin.map(p => p.displayName).join(" + ");
|
|
279
|
+
Logger_1.logger.info("");
|
|
280
|
+
Logger_1.logger.info(chalk_1.default.bold(" " + pluginNames + " Login"));
|
|
281
|
+
Logger_1.logger.info(" " + "─".repeat(40));
|
|
282
|
+
Logger_1.logger.info("");
|
|
283
|
+
// Check if all core credentials provided via flags (non-interactive mode)
|
|
284
|
+
const hasAllCoreFlags = opts.instance && opts.user && opts.password;
|
|
285
|
+
if (hasAllCoreFlags && pluginsToLogin.length === 1 && pluginsToLogin[0].name === "core") {
|
|
286
|
+
Logger_1.logger.info("Validating credentials...");
|
|
287
|
+
const result = await (0, corePlugin_1.validateCoreLogin)(context);
|
|
288
|
+
if (result !== true) {
|
|
289
|
+
Logger_1.logger.error(chalk_1.default.red("✗ " + result));
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
Logger_1.logger.success(chalk_1.default.green("✓ Connected to " + context.env.SN_INSTANCE));
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
for (const plugin of pluginsToLogin) {
|
|
296
|
+
await runLoginPhase(plugin, context);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
// Save env vars
|
|
300
|
+
saveEnvVars(context, pluginsToLogin);
|
|
301
|
+
Logger_1.logger.info("");
|
|
302
|
+
Logger_1.logger.info("You can now use:");
|
|
303
|
+
Logger_1.logger.info(" sinc init — Initialize a new project");
|
|
304
|
+
Logger_1.logger.info(" sinc watch — Watch for changes");
|
|
305
|
+
Logger_1.logger.info(" sinc status — Check instance connection");
|
|
306
|
+
Logger_1.logger.info("");
|
|
307
|
+
}
|
|
308
|
+
catch (e) {
|
|
309
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
310
|
+
Logger_1.logger.error("Login failed: " + message);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.loginCommand = loginCommand;
|
|
4
|
+
const commands_1 = require("./commands");
|
|
5
|
+
const orchestrator_1 = require("./initSystem/orchestrator");
|
|
6
|
+
/**
|
|
7
|
+
* @description Command handler for `sinc login`.
|
|
8
|
+
* Authenticates with ServiceNow and other integrations, saves credentials to .env.
|
|
9
|
+
*/
|
|
10
|
+
async function loginCommand(args) {
|
|
11
|
+
(0, commands_1.setLogLevel)(args);
|
|
12
|
+
await (0, orchestrator_1.runLogin)({
|
|
13
|
+
logLevel: args.logLevel,
|
|
14
|
+
pluginName: args.plugin || undefined,
|
|
15
|
+
all: args.all || false,
|
|
16
|
+
instance: args.instance || undefined,
|
|
17
|
+
user: args.user || undefined,
|
|
18
|
+
password: args.password || undefined,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -71,7 +71,15 @@ jest.mock("chalk", function () {
|
|
|
71
71
|
bold: identity,
|
|
72
72
|
};
|
|
73
73
|
});
|
|
74
|
-
jest.mock("fs")
|
|
74
|
+
jest.mock("fs", function () {
|
|
75
|
+
var actual = jest.requireActual("fs");
|
|
76
|
+
return Object.assign({}, actual, {
|
|
77
|
+
readFileSync: jest.fn(),
|
|
78
|
+
writeFileSync: jest.fn(),
|
|
79
|
+
existsSync: jest.fn(),
|
|
80
|
+
promises: actual.promises,
|
|
81
|
+
});
|
|
82
|
+
});
|
|
75
83
|
jest.mock("child_process");
|
|
76
84
|
// --- Imports (after mocks) ---
|
|
77
85
|
const clickupCommands_1 = require("../clickupCommands");
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// --- Mock setup (must be before imports) ---
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
var mockReaddirSync = jest.fn();
|
|
5
|
+
jest.mock("fs", function () {
|
|
6
|
+
var actual = jest.requireActual("fs");
|
|
7
|
+
return Object.assign({}, actual, {
|
|
8
|
+
readdirSync: mockReaddirSync,
|
|
9
|
+
});
|
|
10
|
+
});
|
|
11
|
+
jest.mock("../Logger", function () {
|
|
12
|
+
return {
|
|
13
|
+
logger: {
|
|
14
|
+
info: jest.fn(),
|
|
15
|
+
error: jest.fn(),
|
|
16
|
+
warn: jest.fn(),
|
|
17
|
+
debug: jest.fn(),
|
|
18
|
+
success: jest.fn(),
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
const discovery_1 = require("../initSystem/discovery");
|
|
23
|
+
const Logger_1 = require("../Logger");
|
|
24
|
+
describe("discoverPlugins", function () {
|
|
25
|
+
beforeEach(function () {
|
|
26
|
+
mockReaddirSync.mockReset();
|
|
27
|
+
// Default: no directories found
|
|
28
|
+
mockReaddirSync.mockImplementation(function () {
|
|
29
|
+
throw new Error("ENOENT");
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
it("returns empty array when no node_modules found", function () {
|
|
33
|
+
const plugins = (0, discovery_1.discoverPlugins)();
|
|
34
|
+
expect(plugins).toEqual([]);
|
|
35
|
+
});
|
|
36
|
+
it("skips sincronia-core and sincronia-types", function () {
|
|
37
|
+
mockReaddirSync.mockReturnValue([
|
|
38
|
+
"sincronia-core",
|
|
39
|
+
"sincronia-types",
|
|
40
|
+
]);
|
|
41
|
+
const plugins = (0, discovery_1.discoverPlugins)();
|
|
42
|
+
expect(plugins).toEqual([]);
|
|
43
|
+
});
|
|
44
|
+
it("warns on plugin load failure", function () {
|
|
45
|
+
mockReaddirSync.mockReturnValue(["sincronia-nonexistent"]);
|
|
46
|
+
(0, discovery_1.discoverPlugins)();
|
|
47
|
+
expect(Logger_1.logger.warn).toHaveBeenCalled();
|
|
48
|
+
});
|
|
49
|
+
it("skips packages without sincPlugin export", function () {
|
|
50
|
+
mockReaddirSync.mockReturnValue(["sincronia-sass-plugin"]);
|
|
51
|
+
// sass-plugin exists but doesn't export sincPlugin — it just won't be added
|
|
52
|
+
const plugins = (0, discovery_1.discoverPlugins)();
|
|
53
|
+
const names = plugins.map(function (p) { return p.name; });
|
|
54
|
+
expect(names).not.toContain("sass-plugin");
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const corePlugin_1 = require("../initSystem/corePlugin");
|
|
4
|
+
describe("normalizeInstance", function () {
|
|
5
|
+
it("strips https:// prefix", function () {
|
|
6
|
+
expect((0, corePlugin_1.normalizeInstance)("https://mycompany.service-now.com")).toBe("mycompany.service-now.com/");
|
|
7
|
+
});
|
|
8
|
+
it("strips http:// prefix", function () {
|
|
9
|
+
expect((0, corePlugin_1.normalizeInstance)("http://mycompany.service-now.com")).toBe("mycompany.service-now.com/");
|
|
10
|
+
});
|
|
11
|
+
it("adds trailing slash if missing", function () {
|
|
12
|
+
expect((0, corePlugin_1.normalizeInstance)("mycompany.service-now.com")).toBe("mycompany.service-now.com/");
|
|
13
|
+
});
|
|
14
|
+
it("preserves trailing slash if present", function () {
|
|
15
|
+
expect((0, corePlugin_1.normalizeInstance)("mycompany.service-now.com/")).toBe("mycompany.service-now.com/");
|
|
16
|
+
});
|
|
17
|
+
it("trims whitespace", function () {
|
|
18
|
+
expect((0, corePlugin_1.normalizeInstance)(" mycompany.service-now.com ")).toBe("mycompany.service-now.com/");
|
|
19
|
+
});
|
|
20
|
+
it("handles full URL with protocol and trailing slash", function () {
|
|
21
|
+
expect((0, corePlugin_1.normalizeInstance)("https://mycompany.service-now.com/")).toBe("mycompany.service-now.com/");
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
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
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
9
|
+
const FileUtils_1 = require("../FileUtils");
|
|
10
|
+
describe("writeEnvVar", function () {
|
|
11
|
+
let tmpDir;
|
|
12
|
+
let envPath;
|
|
13
|
+
beforeEach(function () {
|
|
14
|
+
tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), "sinc-test-"));
|
|
15
|
+
envPath = path_1.default.join(tmpDir, ".env");
|
|
16
|
+
});
|
|
17
|
+
afterEach(function () {
|
|
18
|
+
try {
|
|
19
|
+
fs_1.default.rmSync(tmpDir, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
// cleanup best-effort
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
it("creates a new .env file from scratch", function () {
|
|
26
|
+
(0, FileUtils_1.writeEnvVar)({ key: "FOO", value: "bar", envPath });
|
|
27
|
+
const content = fs_1.default.readFileSync(envPath, "utf8");
|
|
28
|
+
expect(content).toBe("FOO=bar\n");
|
|
29
|
+
});
|
|
30
|
+
it("replaces an existing key", function () {
|
|
31
|
+
fs_1.default.writeFileSync(envPath, "FOO=old\nBAR=keep\n", "utf8");
|
|
32
|
+
(0, FileUtils_1.writeEnvVar)({ key: "FOO", value: "new", envPath });
|
|
33
|
+
const content = fs_1.default.readFileSync(envPath, "utf8");
|
|
34
|
+
expect(content).toContain("FOO=new");
|
|
35
|
+
expect(content).toContain("BAR=keep");
|
|
36
|
+
expect(content).not.toContain("FOO=old");
|
|
37
|
+
});
|
|
38
|
+
it("appends a new key without clobbering existing ones", function () {
|
|
39
|
+
fs_1.default.writeFileSync(envPath, "EXISTING=value\n", "utf8");
|
|
40
|
+
(0, FileUtils_1.writeEnvVar)({ key: "NEW_KEY", value: "new_value", envPath });
|
|
41
|
+
const content = fs_1.default.readFileSync(envPath, "utf8");
|
|
42
|
+
expect(content).toContain("EXISTING=value");
|
|
43
|
+
expect(content).toContain("NEW_KEY=new_value");
|
|
44
|
+
});
|
|
45
|
+
it("handles file with no trailing newline", function () {
|
|
46
|
+
fs_1.default.writeFileSync(envPath, "A=1", "utf8");
|
|
47
|
+
(0, FileUtils_1.writeEnvVar)({ key: "B", value: "2", envPath });
|
|
48
|
+
const content = fs_1.default.readFileSync(envPath, "utf8");
|
|
49
|
+
expect(content).toContain("A=1");
|
|
50
|
+
expect(content).toContain("B=2");
|
|
51
|
+
// Should not produce "A=1B=2" on one line
|
|
52
|
+
const lines = content.trim().split("\n");
|
|
53
|
+
expect(lines.length).toBe(2);
|
|
54
|
+
});
|
|
55
|
+
it("handles keys with regex metacharacters", function () {
|
|
56
|
+
fs_1.default.writeFileSync(envPath, "MY.KEY[0]=old\n", "utf8");
|
|
57
|
+
(0, FileUtils_1.writeEnvVar)({ key: "MY.KEY[0]", value: "new", envPath });
|
|
58
|
+
const content = fs_1.default.readFileSync(envPath, "utf8");
|
|
59
|
+
expect(content).toContain("MY.KEY[0]=new");
|
|
60
|
+
expect(content).not.toContain("MY.KEY[0]=old");
|
|
61
|
+
// Should not have duplicates
|
|
62
|
+
const matches = content.match(/MY\.KEY\[0\]/g);
|
|
63
|
+
expect(matches).toHaveLength(1);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
describe("writeEnvVars", function () {
|
|
67
|
+
let tmpDir;
|
|
68
|
+
let envPath;
|
|
69
|
+
beforeEach(function () {
|
|
70
|
+
tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), "sinc-test-"));
|
|
71
|
+
envPath = path_1.default.join(tmpDir, ".env");
|
|
72
|
+
});
|
|
73
|
+
afterEach(function () {
|
|
74
|
+
try {
|
|
75
|
+
fs_1.default.rmSync(tmpDir, { recursive: true });
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
// cleanup best-effort
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
it("writes multiple vars in a single call", function () {
|
|
82
|
+
(0, FileUtils_1.writeEnvVars)({
|
|
83
|
+
vars: [
|
|
84
|
+
{ key: "A", value: "1" },
|
|
85
|
+
{ key: "B", value: "2" },
|
|
86
|
+
{ key: "C", value: "3" },
|
|
87
|
+
],
|
|
88
|
+
envPath,
|
|
89
|
+
});
|
|
90
|
+
const content = fs_1.default.readFileSync(envPath, "utf8");
|
|
91
|
+
expect(content).toContain("A=1");
|
|
92
|
+
expect(content).toContain("B=2");
|
|
93
|
+
expect(content).toContain("C=3");
|
|
94
|
+
});
|
|
95
|
+
it("merges into existing file preserving unrelated keys", function () {
|
|
96
|
+
fs_1.default.writeFileSync(envPath, "KEEP=yes\nUPDATE=old\n", "utf8");
|
|
97
|
+
(0, FileUtils_1.writeEnvVars)({
|
|
98
|
+
vars: [
|
|
99
|
+
{ key: "UPDATE", value: "new" },
|
|
100
|
+
{ key: "ADDED", value: "fresh" },
|
|
101
|
+
],
|
|
102
|
+
envPath,
|
|
103
|
+
});
|
|
104
|
+
const content = fs_1.default.readFileSync(envPath, "utf8");
|
|
105
|
+
expect(content).toContain("KEEP=yes");
|
|
106
|
+
expect(content).toContain("UPDATE=new");
|
|
107
|
+
expect(content).toContain("ADDED=fresh");
|
|
108
|
+
expect(content).not.toContain("UPDATE=old");
|
|
109
|
+
});
|
|
110
|
+
it("handles empty file", function () {
|
|
111
|
+
fs_1.default.writeFileSync(envPath, "", "utf8");
|
|
112
|
+
(0, FileUtils_1.writeEnvVars)({
|
|
113
|
+
vars: [{ key: "X", value: "y" }],
|
|
114
|
+
envPath,
|
|
115
|
+
});
|
|
116
|
+
const content = fs_1.default.readFileSync(envPath, "utf8");
|
|
117
|
+
expect(content).toBe("X=y\n");
|
|
118
|
+
});
|
|
119
|
+
});
|