@hasna/connectors 0.3.12 → 0.3.14

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/bin/index.js CHANGED
@@ -6833,7 +6833,7 @@ var PRESETS = {
6833
6833
  commerce: { description: "Commerce and finance", connectors: ["stripe", "shopify", "revolut", "mercury", "pandadoc"] }
6834
6834
  };
6835
6835
  var program2 = new Command;
6836
- program2.name("connectors").description("Install API connectors for your project").version("0.3.12").enablePositionalOptions();
6836
+ program2.name("connectors").description("Install API connectors for your project").version("0.3.14").enablePositionalOptions();
6837
6837
  program2.command("interactive", { isDefault: true }).alias("i").description("Interactive connector browser").action(() => {
6838
6838
  if (!isTTY) {
6839
6839
  console.log(`Non-interactive environment detected. Use a subcommand:
@@ -7951,27 +7951,48 @@ program2.command("export").option("-o, --output <file>", "Write to file instead
7951
7951
  if (!statSync3(entryPath).isDirectory() || !entry.startsWith("connect-"))
7952
7952
  continue;
7953
7953
  const connectorName = entry.replace(/^connect-/, "");
7954
+ let credentials = undefined;
7955
+ const credentialsPath = join6(entryPath, "credentials.json");
7956
+ if (existsSync6(credentialsPath)) {
7957
+ try {
7958
+ credentials = JSON.parse(readFileSync5(credentialsPath, "utf-8"));
7959
+ } catch {}
7960
+ }
7954
7961
  const profilesDir = join6(entryPath, "profiles");
7955
- if (!existsSync6(profilesDir))
7962
+ if (!existsSync6(profilesDir) && !credentials)
7956
7963
  continue;
7957
7964
  const profiles = {};
7958
- for (const pEntry of readdirSync4(profilesDir)) {
7959
- const pPath = join6(profilesDir, pEntry);
7960
- if (statSync3(pPath).isFile() && pEntry.endsWith(".json")) {
7961
- try {
7962
- profiles[pEntry.replace(/\.json$/, "")] = JSON.parse(readFileSync5(pPath, "utf-8"));
7963
- } catch {}
7964
- } else if (statSync3(pPath).isDirectory()) {
7965
- const configPath = join6(pPath, "config.json");
7966
- if (existsSync6(configPath)) {
7965
+ if (existsSync6(profilesDir)) {
7966
+ for (const pEntry of readdirSync4(profilesDir)) {
7967
+ const pPath = join6(profilesDir, pEntry);
7968
+ if (statSync3(pPath).isFile() && pEntry.endsWith(".json")) {
7967
7969
  try {
7968
- profiles[pEntry] = JSON.parse(readFileSync5(configPath, "utf-8"));
7970
+ profiles[pEntry.replace(/\.json$/, "")] = JSON.parse(readFileSync5(pPath, "utf-8"));
7969
7971
  } catch {}
7972
+ } else if (statSync3(pPath).isDirectory()) {
7973
+ const configPath = join6(pPath, "config.json");
7974
+ const tokensPath = join6(pPath, "tokens.json");
7975
+ let merged = {};
7976
+ if (existsSync6(configPath)) {
7977
+ try {
7978
+ merged = { ...merged, ...JSON.parse(readFileSync5(configPath, "utf-8")) };
7979
+ } catch {}
7980
+ }
7981
+ if (existsSync6(tokensPath)) {
7982
+ try {
7983
+ merged = { ...merged, ...JSON.parse(readFileSync5(tokensPath, "utf-8")) };
7984
+ } catch {}
7985
+ }
7986
+ if (Object.keys(merged).length > 0)
7987
+ profiles[pEntry] = merged;
7970
7988
  }
7971
7989
  }
7972
7990
  }
7973
- if (Object.keys(profiles).length > 0)
7974
- result[connectorName] = { profiles };
7991
+ const connectorData = { profiles };
7992
+ if (credentials)
7993
+ connectorData.credentials = credentials;
7994
+ if (Object.keys(profiles).length > 0 || credentials)
7995
+ result[connectorName] = connectorData;
7975
7996
  }
7976
7997
  }
7977
7998
  const exportPayload = options.includeSecrets ? { connectors: result, exportedAt: new Date().toISOString() } : { connectors: redactSecrets(result), exportedAt: new Date().toISOString(), redacted: true };
@@ -8031,9 +8052,15 @@ program2.command("import").argument("<file>", "JSON backup file to import (use -
8031
8052
  for (const [connectorName, connData] of Object.entries(data.connectors)) {
8032
8053
  if (!/^[a-z0-9-]+$/.test(connectorName))
8033
8054
  continue;
8055
+ const connectorDir = join6(connectDir, `connect-${connectorName}`);
8056
+ if (connData.credentials && typeof connData.credentials === "object") {
8057
+ mkdirSync4(connectorDir, { recursive: true });
8058
+ writeFileSync4(join6(connectorDir, "credentials.json"), JSON.stringify(connData.credentials, null, 2));
8059
+ imported++;
8060
+ }
8034
8061
  if (!connData.profiles || typeof connData.profiles !== "object")
8035
8062
  continue;
8036
- const profilesDir = join6(connectDir, `connect-${connectorName}`, "profiles");
8063
+ const profilesDir = join6(connectorDir, "profiles");
8037
8064
  for (const [profileName, config] of Object.entries(connData.profiles)) {
8038
8065
  if (!config || typeof config !== "object")
8039
8066
  continue;
@@ -8049,7 +8076,7 @@ program2.command("import").argument("<file>", "JSON backup file to import (use -
8049
8076
  }
8050
8077
  });
8051
8078
  program2.command("upgrade").alias("self-update").option("--check", "Only check for updates, don't install", false).option("--json", "Output as JSON", false).description("Check for updates and upgrade to the latest version").action(async (options) => {
8052
- const currentVersion = "0.3.1";
8079
+ const currentVersion = program2.version();
8053
8080
  try {
8054
8081
  const res = await fetch("https://registry.npmjs.org/@hasna/connectors/latest");
8055
8082
  if (!res.ok)
package/bin/mcp.js CHANGED
@@ -20315,7 +20315,7 @@ async function getConnectorCommandHelp(name, command) {
20315
20315
  loadConnectorVersions();
20316
20316
  var server = new McpServer({
20317
20317
  name: "connectors",
20318
- version: "0.3.12"
20318
+ version: "0.3.14"
20319
20319
  });
20320
20320
  server.registerTool("search_connectors", {
20321
20321
  title: "Search Connectors",
@@ -143,22 +143,41 @@ export function deleteProfile(profile: string): boolean {
143
143
  }
144
144
 
145
145
  /**
146
- * Load profile config
146
+ * Load profile config — checks both flat and directory patterns, merges tokens.json
147
147
  */
148
148
  export function loadProfile(profile?: string): ProfileConfig {
149
149
  ensureConfigDir();
150
150
  const profileName = profile || getCurrentProfile();
151
- const profilePath = getProfilePath(profileName);
152
151
 
153
- if (!existsSync(profilePath)) {
154
- return {};
152
+ let config: ProfileConfig = {};
153
+
154
+ // Pattern 1: profiles/<name>.json (flat file)
155
+ const flatPath = getProfilePath(profileName);
156
+ if (existsSync(flatPath)) {
157
+ try { config = JSON.parse(readFileSync(flatPath, 'utf-8')); } catch {}
155
158
  }
156
159
 
157
- try {
158
- return JSON.parse(readFileSync(profilePath, 'utf-8'));
159
- } catch {
160
- return {};
160
+ // Pattern 2: profiles/<name>/config.json (directory)
161
+ const dirConfigPath = join(PROFILES_DIR, profileName, 'config.json');
162
+ if (existsSync(dirConfigPath)) {
163
+ try {
164
+ const dirConfig = JSON.parse(readFileSync(dirConfigPath, 'utf-8'));
165
+ config = { ...config, ...dirConfig };
166
+ } catch {}
161
167
  }
168
+
169
+ // Pattern 3: profiles/<name>/tokens.json (OAuth tokens)
170
+ const tokensPath = join(PROFILES_DIR, profileName, 'tokens.json');
171
+ if (existsSync(tokensPath)) {
172
+ try {
173
+ const tokens = JSON.parse(readFileSync(tokensPath, 'utf-8'));
174
+ if (tokens.accessToken && !config.accessToken) config.accessToken = tokens.accessToken;
175
+ if (tokens.refreshToken && !config.refreshToken) config.refreshToken = tokens.refreshToken;
176
+ if (tokens.expiresAt && !config.expiresAt) config.expiresAt = tokens.expiresAt;
177
+ } catch {}
178
+ }
179
+
180
+ return config;
162
181
  }
163
182
 
164
183
  /**
@@ -136,22 +136,41 @@ export function deleteProfile(profile: string): boolean {
136
136
  }
137
137
 
138
138
  /**
139
- * Load profile config
139
+ * Load profile config — checks both flat and directory patterns, merges tokens.json
140
140
  */
141
141
  export function loadProfile(profile?: string): ProfileConfig {
142
142
  ensureConfigDir();
143
143
  const profileName = profile || getCurrentProfile();
144
- const profilePath = getProfilePath(profileName);
145
144
 
146
- if (!existsSync(profilePath)) {
147
- return {};
145
+ let config: ProfileConfig = {};
146
+
147
+ // Pattern 1: profiles/<name>.json (flat file)
148
+ const flatPath = getProfilePath(profileName);
149
+ if (existsSync(flatPath)) {
150
+ try { config = JSON.parse(readFileSync(flatPath, 'utf-8')); } catch {}
148
151
  }
149
152
 
150
- try {
151
- return JSON.parse(readFileSync(profilePath, 'utf-8'));
152
- } catch {
153
- return {};
153
+ // Pattern 2: profiles/<name>/config.json (directory)
154
+ const dirConfigPath = join(PROFILES_DIR, profileName, 'config.json');
155
+ if (existsSync(dirConfigPath)) {
156
+ try {
157
+ const dirConfig = JSON.parse(readFileSync(dirConfigPath, 'utf-8'));
158
+ config = { ...config, ...dirConfig };
159
+ } catch {}
154
160
  }
161
+
162
+ // Pattern 3: profiles/<name>/tokens.json (OAuth tokens)
163
+ const tokensPath = join(PROFILES_DIR, profileName, 'tokens.json');
164
+ if (existsSync(tokensPath)) {
165
+ try {
166
+ const tokens = JSON.parse(readFileSync(tokensPath, 'utf-8'));
167
+ if (tokens.accessToken && !config.accessToken) config.accessToken = tokens.accessToken;
168
+ if (tokens.refreshToken && !config.refreshToken) config.refreshToken = tokens.refreshToken;
169
+ if (tokens.expiresAt && !config.tokenExpiresAt) config.tokenExpiresAt = tokens.expiresAt;
170
+ } catch {}
171
+ }
172
+
173
+ return config;
155
174
  }
156
175
 
157
176
  /**
@@ -259,18 +259,37 @@ export function ensureImportsDir(): string {
259
259
 
260
260
  export function loadConfig(): CliConfig {
261
261
  ensureConfigDir();
262
- const configFile = join(getConfigDirInternal(), 'config.json');
263
262
 
264
- if (!existsSync(configFile)) {
265
- return {};
263
+ let config: CliConfig = {};
264
+
265
+ // Pattern 1: profiles/<name>.json (flat file)
266
+ const profileName = getCurrentProfile();
267
+ const flatPath = join(getProfilesDir(), `${profileName}.json`);
268
+ if (existsSync(flatPath)) {
269
+ try { config = JSON.parse(readFileSync(flatPath, 'utf-8')); } catch {}
266
270
  }
267
271
 
268
- try {
269
- const content = readFileSync(configFile, 'utf-8');
270
- return JSON.parse(content);
271
- } catch {
272
- return {};
272
+ // Pattern 2: profiles/<name>/config.json (directory)
273
+ const configFile = join(getConfigDirInternal(), 'config.json');
274
+ if (existsSync(configFile)) {
275
+ try {
276
+ const dirConfig = JSON.parse(readFileSync(configFile, 'utf-8'));
277
+ config = { ...config, ...dirConfig };
278
+ } catch {}
279
+ }
280
+
281
+ // Pattern 3: profiles/<name>/tokens.json (OAuth tokens)
282
+ const tokensPath = join(getConfigDirInternal(), 'tokens.json');
283
+ if (existsSync(tokensPath)) {
284
+ try {
285
+ const tokens = JSON.parse(readFileSync(tokensPath, 'utf-8'));
286
+ if (tokens.accessToken && !config.tokens) {
287
+ config.tokens = tokens;
288
+ }
289
+ } catch {}
273
290
  }
291
+
292
+ return config;
274
293
  }
275
294
 
276
295
  export function saveConfig(config: CliConfig): void {
@@ -150,22 +150,41 @@ export function deleteProfile(profile: string): boolean {
150
150
  }
151
151
 
152
152
  /**
153
- * Load profile config
153
+ * Load profile config — checks both flat and directory patterns, merges tokens.json
154
154
  */
155
155
  export function loadProfile(profile?: string): ProfileConfig {
156
156
  ensureConfigDir();
157
157
  const profileName = profile || getCurrentProfile();
158
- const profilePath = getProfilePath(profileName);
159
158
 
160
- if (!existsSync(profilePath)) {
161
- return {};
159
+ let config: ProfileConfig = {};
160
+
161
+ // Pattern 1: profiles/<name>.json (flat file)
162
+ const flatPath = getProfilePath(profileName);
163
+ if (existsSync(flatPath)) {
164
+ try { config = JSON.parse(readFileSync(flatPath, 'utf-8')); } catch {}
162
165
  }
163
166
 
164
- try {
165
- return JSON.parse(readFileSync(profilePath, 'utf-8'));
166
- } catch {
167
- return {};
167
+ // Pattern 2: profiles/<name>/config.json (directory)
168
+ const dirConfigPath = join(PROFILES_DIR, profileName, 'config.json');
169
+ if (existsSync(dirConfigPath)) {
170
+ try {
171
+ const dirConfig = JSON.parse(readFileSync(dirConfigPath, 'utf-8'));
172
+ config = { ...config, ...dirConfig };
173
+ } catch {}
168
174
  }
175
+
176
+ // Pattern 3: profiles/<name>/tokens.json (OAuth tokens)
177
+ const tokensPath = join(PROFILES_DIR, profileName, 'tokens.json');
178
+ if (existsSync(tokensPath)) {
179
+ try {
180
+ const tokens = JSON.parse(readFileSync(tokensPath, 'utf-8'));
181
+ if (tokens.accessToken && !config.accessToken) config.accessToken = tokens.accessToken;
182
+ if (tokens.refreshToken && !config.refreshToken) config.refreshToken = tokens.refreshToken;
183
+ if (tokens.expiresAt && !config.expiresAt) config.expiresAt = tokens.expiresAt;
184
+ } catch {}
185
+ }
186
+
187
+ return config;
169
188
  }
170
189
 
171
190
  /**
@@ -167,18 +167,36 @@ export function ensureConfigDir(): void {
167
167
  export function loadProfile(profile?: string): ProfileConfig {
168
168
  ensureConfigDir();
169
169
  const profileName = profile || getCurrentProfile();
170
- const profileDir = join(getProfilesDir(), profileName);
171
- const configFile = join(profileDir, 'config.json');
172
170
 
173
- if (!existsSync(configFile)) {
174
- return {};
171
+ let config: ProfileConfig = {};
172
+
173
+ // Pattern 1: profiles/<name>.json (flat file)
174
+ const flatPath = join(getProfilesDir(), `${profileName}.json`);
175
+ if (existsSync(flatPath)) {
176
+ try { config = JSON.parse(readFileSync(flatPath, 'utf-8')); } catch {}
175
177
  }
176
178
 
177
- try {
178
- return JSON.parse(readFileSync(configFile, 'utf-8'));
179
- } catch {
180
- return {};
179
+ // Pattern 2: profiles/<name>/config.json (directory)
180
+ const dirConfigPath = join(getProfilesDir(), profileName, 'config.json');
181
+ if (existsSync(dirConfigPath)) {
182
+ try {
183
+ const dirConfig = JSON.parse(readFileSync(dirConfigPath, 'utf-8'));
184
+ config = { ...config, ...dirConfig };
185
+ } catch {}
181
186
  }
187
+
188
+ // Pattern 3: profiles/<name>/tokens.json (OAuth tokens)
189
+ const tokensPath = join(getProfilesDir(), profileName, 'tokens.json');
190
+ if (existsSync(tokensPath)) {
191
+ try {
192
+ const tokens = JSON.parse(readFileSync(tokensPath, 'utf-8'));
193
+ if (tokens.accessToken && !config.accessToken) config.accessToken = tokens.accessToken;
194
+ if (tokens.refreshToken && !config.refreshToken) config.refreshToken = tokens.refreshToken;
195
+ if (tokens.expiresAt && !config.tokenExpiry) config.tokenExpiry = tokens.expiresAt;
196
+ } catch {}
197
+ }
198
+
199
+ return config;
182
200
  }
183
201
 
184
202
  export function saveProfile(config: ProfileConfig, profile?: string): void {
@@ -77,10 +77,10 @@ export function setCurrentProfile(profile: string): void {
77
77
  }
78
78
 
79
79
  /**
80
- * Check if a profile exists
80
+ * Check if a profile exists (flat file or directory pattern)
81
81
  */
82
82
  export function profileExists(profile: string): boolean {
83
- return existsSync(getProfilePath(profile));
83
+ return existsSync(getProfilePath(profile)) || existsSync(join(PROFILES_DIR, profile));
84
84
  }
85
85
 
86
86
  /**
@@ -93,10 +93,16 @@ export function listProfiles(): string[] {
93
93
  return [];
94
94
  }
95
95
 
96
- return readdirSync(PROFILES_DIR)
97
- .filter(f => f.endsWith('.json'))
98
- .map(f => f.replace('.json', ''))
99
- .sort();
96
+ const seen = new Set<string>();
97
+ const entries = readdirSync(PROFILES_DIR, { withFileTypes: true });
98
+ for (const entry of entries) {
99
+ if (entry.isDirectory()) {
100
+ seen.add(entry.name);
101
+ } else if (entry.name.endsWith('.json')) {
102
+ seen.add(entry.name.replace('.json', ''));
103
+ }
104
+ }
105
+ return Array.from(seen).sort();
100
106
  }
101
107
 
102
108
  /**
@@ -135,27 +141,53 @@ export function deleteProfile(profile: string): boolean {
135
141
  setCurrentProfile(DEFAULT_PROFILE);
136
142
  }
137
143
 
138
- rmSync(getProfilePath(profile));
144
+ // Remove flat file pattern
145
+ const flatPath = getProfilePath(profile);
146
+ if (existsSync(flatPath)) {
147
+ rmSync(flatPath);
148
+ }
149
+
150
+ // Remove directory pattern
151
+ const dirPath = join(PROFILES_DIR, profile);
152
+ if (existsSync(dirPath)) {
153
+ rmSync(dirPath, { recursive: true });
154
+ }
155
+
139
156
  return true;
140
157
  }
141
158
 
142
159
  /**
143
- * Load profile config
160
+ * Load profile config.
161
+ * Checks both flat (profiles/<name>.json) and directory (profiles/<name>/config.json)
162
+ * patterns. Flat pattern takes precedence when both exist.
144
163
  */
145
164
  export function loadProfile(profile?: string): ProfileConfig {
146
165
  ensureConfigDir();
147
166
  const profileName = profile || getCurrentProfile();
148
- const profilePath = getProfilePath(profileName);
149
167
 
150
- if (!existsSync(profilePath)) {
151
- return {};
168
+ let config: ProfileConfig = {};
169
+
170
+ // Pattern 2: profiles/<name>/config.json (directory pattern, used by dashboard)
171
+ const dirConfigPath = join(PROFILES_DIR, profileName, 'config.json');
172
+ if (existsSync(dirConfigPath)) {
173
+ try {
174
+ config = JSON.parse(readFileSync(dirConfigPath, 'utf-8'));
175
+ } catch {
176
+ // ignore
177
+ }
152
178
  }
153
179
 
154
- try {
155
- return JSON.parse(readFileSync(profilePath, 'utf-8'));
156
- } catch {
157
- return {};
180
+ // Pattern 1: profiles/<name>.json (flat pattern, used by CLI)
181
+ const profilePath = getProfilePath(profileName);
182
+ if (existsSync(profilePath)) {
183
+ try {
184
+ config = { ...config, ...JSON.parse(readFileSync(profilePath, 'utf-8')) };
185
+ } catch {
186
+ // ignore
187
+ }
158
188
  }
189
+
190
+ return config;
159
191
  }
160
192
 
161
193
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/connectors",
3
- "version": "0.3.12",
3
+ "version": "0.3.14",
4
4
  "description": "Open source connector library - Install API connectors with a single command",
5
5
  "type": "module",
6
6
  "bin": {