@tenonhq/sincronia-core 0.0.69 → 0.0.72

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.
@@ -87,8 +87,8 @@ exports.corePlugin = {
87
87
  key: "app",
88
88
  label: "Selecting ServiceNow application",
89
89
  run: async (context) => {
90
- const instanceUrl = normalizeInstance(context.env.SN_INSTANCE);
91
- const client = (0, snClient_1.snClient)(instanceUrl, context.env.SN_USER, context.env.SN_PASSWORD);
90
+ const baseUrl = instanceBaseUrl(context.env.SN_INSTANCE);
91
+ const client = (0, snClient_1.snClient)(baseUrl, context.env.SN_USER, context.env.SN_PASSWORD);
92
92
  Logger_1.logger.info("Fetching application list...");
93
93
  const apps = await (0, snClient_1.unwrapSNResponse)(client.getAppList());
94
94
  if (apps.length === 0) {
@@ -167,8 +167,8 @@ exports.corePlugin = {
167
167
  }
168
168
  // Download application files — errors propagate to orchestrator
169
169
  Logger_1.logger.info("Downloading " + scope + "...");
170
- const instanceUrl = normalizeInstance(context.env.SN_INSTANCE);
171
- const client = (0, snClient_1.snClient)(instanceUrl, context.env.SN_USER, context.env.SN_PASSWORD);
170
+ const baseUrl = instanceBaseUrl(context.env.SN_INSTANCE);
171
+ const client = (0, snClient_1.snClient)(baseUrl, context.env.SN_USER, context.env.SN_PASSWORD);
172
172
  const config = ConfigManager.getConfig();
173
173
  const man = await (0, snClient_1.unwrapSNResponse)(client.getManifest(scope, config, true));
174
174
  await AppUtils.processManifest(man);
@@ -191,20 +191,38 @@ async function validateCoreLogin(context) {
191
191
  return "Missing required credentials";
192
192
  }
193
193
  const instanceUrl = normalizeInstance(instance);
194
+ const baseUrl = instanceBaseUrl(instance);
194
195
  try {
195
- const client = (0, snClient_1.snClient)(instanceUrl, user, password);
196
+ const client = (0, snClient_1.snClient)(baseUrl, user, password);
196
197
  await (0, snClient_1.unwrapSNResponse)(client.getAppList());
197
198
  context.env.SN_INSTANCE = instanceUrl;
198
199
  return true;
199
200
  }
200
201
  catch (e) {
201
- return "Connection failed check your instance URL, username, and password.";
202
+ const msg = e instanceof Error ? e.message : String(e);
203
+ const status = e && e.response && e.response.status;
204
+ if (msg.includes("Invalid URL") || msg.includes("ENOTFOUND") || msg.includes("getaddrinfo")) {
205
+ return "Instance not found — check the URL (got: " + instanceUrl + ")";
206
+ }
207
+ if (status === 401 || msg.includes("401")) {
208
+ return "Invalid username or password.";
209
+ }
210
+ if (status === 403 || msg.includes("403")) {
211
+ return "Access denied — user may lack required roles.";
212
+ }
213
+ if (msg.includes("ECONNREFUSED") || msg.includes("ETIMEDOUT") || msg.includes("ECONNRESET")) {
214
+ return "Could not reach " + instanceUrl + " — check network connectivity.";
215
+ }
216
+ return "Connection failed: " + msg;
202
217
  }
203
218
  }
204
219
  function normalizeInstance(instance) {
205
220
  let url = instance.trim().replace("https://", "").replace("http://", "");
206
- if (!url.endsWith("/")) {
207
- url += "/";
221
+ if (url.endsWith("/")) {
222
+ url = url.slice(0, -1);
208
223
  }
209
224
  return url;
210
225
  }
226
+ function instanceBaseUrl(instance) {
227
+ return "https://" + normalizeInstance(instance) + "/";
228
+ }
@@ -35,10 +35,7 @@ function buildInitContext(plugins) {
35
35
  // No .env yet — starting fresh
36
36
  }
37
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
- ]);
38
+ const pluginEnvKeys = plugins.flatMap(p => (p.login || []).map(h => h.envKey));
42
39
  pluginEnvKeys.forEach(key => {
43
40
  if (process.env[key] && !env[key]) {
44
41
  env[key] = process.env[key];
@@ -76,10 +73,7 @@ async function promptPluginSelection(externalPlugins) {
76
73
  const selected = externalPlugins.filter(p => selectedNames.has(p.name));
77
74
  return [corePlugin_1.corePlugin, ...selected];
78
75
  }
79
- async function runLoginPhase(plugin, context) {
80
- const hooks = plugin.login;
81
- if (!hooks || hooks.length === 0)
82
- return;
76
+ async function collectLoginHooks(hooks, context) {
83
77
  for (const hook of hooks) {
84
78
  const existingValue = context.env[hook.envKey] || "";
85
79
  // Show instructions if provided
@@ -112,7 +106,63 @@ async function runLoginPhase(plugin, context) {
112
106
  const answer = await inquirer_1.default.prompt([promptConfig]);
113
107
  context.env[hook.envKey] = answer.value.trim();
114
108
  }
115
- // Run per-hook validation if defined
109
+ }
110
+ async function runLoginPhase(plugin, context) {
111
+ const hooks = plugin.login;
112
+ if (!hooks || hooks.length === 0)
113
+ return;
114
+ // Core plugin: retry loop with specific error messages
115
+ if (plugin.name === "core") {
116
+ while (true) {
117
+ await collectLoginHooks(hooks, context);
118
+ // Run per-hook validation
119
+ let hookFailed = false;
120
+ for (const hook of hooks) {
121
+ if (hook.validate) {
122
+ const result = await hook.validate(context.env[hook.envKey], context);
123
+ if (result !== true) {
124
+ Logger_1.logger.error(chalk_1.default.red("✗ " + result));
125
+ hookFailed = true;
126
+ break;
127
+ }
128
+ }
129
+ }
130
+ if (hookFailed) {
131
+ const retry = await inquirer_1.default.prompt([{
132
+ type: "confirm",
133
+ name: "again",
134
+ message: "Try again?",
135
+ default: true,
136
+ }]);
137
+ if (!retry.again) {
138
+ throw new Error("Login cancelled");
139
+ }
140
+ context.env.SN_PASSWORD = "";
141
+ continue;
142
+ }
143
+ Logger_1.logger.info("Validating credentials...");
144
+ const coreResult = await (0, corePlugin_1.validateCoreLogin)(context);
145
+ if (coreResult === true) {
146
+ Logger_1.logger.success(chalk_1.default.green("✓ Connected to " + context.env.SN_INSTANCE));
147
+ return;
148
+ }
149
+ Logger_1.logger.error(chalk_1.default.red("✗ " + coreResult));
150
+ Logger_1.logger.info("");
151
+ const retry = await inquirer_1.default.prompt([{
152
+ type: "confirm",
153
+ name: "again",
154
+ message: "Try again?",
155
+ default: true,
156
+ }]);
157
+ if (!retry.again) {
158
+ throw new Error("Login cancelled");
159
+ }
160
+ // Clear password so it re-prompts; instance and user show as defaults
161
+ context.env.SN_PASSWORD = "";
162
+ }
163
+ }
164
+ // Non-core plugins: original behavior (no retry loop)
165
+ await collectLoginHooks(hooks, context);
116
166
  for (const hook of hooks) {
117
167
  if (hook.validate) {
118
168
  const result = await hook.validate(context.env[hook.envKey], context);
@@ -122,16 +172,6 @@ async function runLoginPhase(plugin, context) {
122
172
  }
123
173
  }
124
174
  }
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
175
  }
136
176
  async function runConfigurePhase(plugin, context) {
137
177
  const hooks = plugin.configure;
@@ -3,21 +3,21 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const corePlugin_1 = require("../initSystem/corePlugin");
4
4
  describe("normalizeInstance", function () {
5
5
  it("strips https:// prefix", function () {
6
- expect((0, corePlugin_1.normalizeInstance)("https://mycompany.service-now.com")).toBe("mycompany.service-now.com/");
6
+ expect((0, corePlugin_1.normalizeInstance)("https://mycompany.service-now.com")).toBe("mycompany.service-now.com");
7
7
  });
8
8
  it("strips http:// prefix", function () {
9
- expect((0, corePlugin_1.normalizeInstance)("http://mycompany.service-now.com")).toBe("mycompany.service-now.com/");
9
+ expect((0, corePlugin_1.normalizeInstance)("http://mycompany.service-now.com")).toBe("mycompany.service-now.com");
10
10
  });
11
- it("adds trailing slash if missing", function () {
12
- expect((0, corePlugin_1.normalizeInstance)("mycompany.service-now.com")).toBe("mycompany.service-now.com/");
11
+ it("returns bare hostname without trailing slash", function () {
12
+ expect((0, corePlugin_1.normalizeInstance)("mycompany.service-now.com")).toBe("mycompany.service-now.com");
13
13
  });
14
- it("preserves trailing slash if present", function () {
15
- expect((0, corePlugin_1.normalizeInstance)("mycompany.service-now.com/")).toBe("mycompany.service-now.com/");
14
+ it("strips trailing slash if present", function () {
15
+ expect((0, corePlugin_1.normalizeInstance)("mycompany.service-now.com/")).toBe("mycompany.service-now.com");
16
16
  });
17
17
  it("trims whitespace", function () {
18
- expect((0, corePlugin_1.normalizeInstance)(" mycompany.service-now.com ")).toBe("mycompany.service-now.com/");
18
+ expect((0, corePlugin_1.normalizeInstance)(" mycompany.service-now.com ")).toBe("mycompany.service-now.com");
19
19
  });
20
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/");
21
+ expect((0, corePlugin_1.normalizeInstance)("https://mycompany.service-now.com/")).toBe("mycompany.service-now.com");
22
22
  });
23
23
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenonhq/sincronia-core",
3
- "version": "0.0.69",
3
+ "version": "0.0.72",
4
4
  "description": "Next-gen file syncer",
5
5
  "license": "GPL-3.0",
6
6
  "main": "./dist/index.js",