@base44-preview/cli 0.0.15-pr.99.a46f627 → 0.0.15-pr.99.a90e72b
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/README.md +24 -8
- package/dist/cli/index.js +445 -32
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -72,29 +72,45 @@ base44 deploy
|
|
|
72
72
|
|
|
73
73
|
### Connectors
|
|
74
74
|
|
|
75
|
-
Manage OAuth integrations to connect your app with external services.
|
|
75
|
+
Manage OAuth integrations to connect your app with external services. Connectors are tracked in a local `connectors.jsonc` file and synced with the backend.
|
|
76
76
|
|
|
77
77
|
| Command | Description |
|
|
78
78
|
|---------|-------------|
|
|
79
|
-
| `base44 connectors:add [type]` |
|
|
80
|
-
| `base44 connectors:list` | List all connected
|
|
81
|
-
| `base44 connectors:
|
|
79
|
+
| `base44 connectors:add [type]` | Add and connect an OAuth integration |
|
|
80
|
+
| `base44 connectors:list` | List all connectors (local and connected) |
|
|
81
|
+
| `base44 connectors:push` | Connect all pending integrations from local config |
|
|
82
|
+
| `base44 connectors:remove [type]` | Remove an integration |
|
|
82
83
|
| `base44 connectors:remove [type] --hard` | Permanently remove an integration |
|
|
83
84
|
|
|
84
85
|
**Supported integrations:** Slack, Google Calendar, Google Drive, Gmail, Google Sheets, Google Docs, Google Slides, Notion, Salesforce, HubSpot, LinkedIn, TikTok
|
|
85
86
|
|
|
86
|
-
**Example:**
|
|
87
|
+
**Example workflow:**
|
|
87
88
|
```bash
|
|
88
|
-
#
|
|
89
|
+
# Add connectors (saves to connectors.jsonc and opens OAuth)
|
|
89
90
|
base44 connectors:add slack
|
|
91
|
+
base44 connectors:add googlecalendar
|
|
90
92
|
|
|
91
|
-
# List connected
|
|
93
|
+
# List connectors showing local vs connected status
|
|
92
94
|
base44 connectors:list
|
|
95
|
+
# Output:
|
|
96
|
+
# ● Slack - user@example.com
|
|
97
|
+
# ○ Google Calendar (not connected)
|
|
93
98
|
|
|
94
|
-
#
|
|
99
|
+
# Connect all pending integrations
|
|
100
|
+
base44 connectors:push
|
|
101
|
+
|
|
102
|
+
# Remove a connector
|
|
95
103
|
base44 connectors:remove slack
|
|
96
104
|
```
|
|
97
105
|
|
|
106
|
+
**Local configuration** (`base44/connectors.jsonc`):
|
|
107
|
+
```jsonc
|
|
108
|
+
{
|
|
109
|
+
"slack": {},
|
|
110
|
+
"googlecalendar": { "scopes": ["calendar.readonly"] }
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
98
114
|
Once connected, use the SDK's `connectors.getAccessToken()` to retrieve tokens:
|
|
99
115
|
```javascript
|
|
100
116
|
const token = await base44.connectors.getAccessToken("slack");
|
package/dist/cli/index.js
CHANGED
|
@@ -5819,6 +5819,97 @@ function handleTupleResult(result, final, index) {
|
|
|
5819
5819
|
if (result.issues.length) final.issues.push(...prefixIssues(index, result.issues));
|
|
5820
5820
|
final.value[index] = result.value;
|
|
5821
5821
|
}
|
|
5822
|
+
const $ZodRecord = /* @__PURE__ */ $constructor("$ZodRecord", (inst, def) => {
|
|
5823
|
+
$ZodType.init(inst, def);
|
|
5824
|
+
inst._zod.parse = (payload, ctx) => {
|
|
5825
|
+
const input = payload.value;
|
|
5826
|
+
if (!isPlainObject$1(input)) {
|
|
5827
|
+
payload.issues.push({
|
|
5828
|
+
expected: "record",
|
|
5829
|
+
code: "invalid_type",
|
|
5830
|
+
input,
|
|
5831
|
+
inst
|
|
5832
|
+
});
|
|
5833
|
+
return payload;
|
|
5834
|
+
}
|
|
5835
|
+
const proms = [];
|
|
5836
|
+
const values = def.keyType._zod.values;
|
|
5837
|
+
if (values) {
|
|
5838
|
+
payload.value = {};
|
|
5839
|
+
const recordKeys = /* @__PURE__ */ new Set();
|
|
5840
|
+
for (const key of values) if (typeof key === "string" || typeof key === "number" || typeof key === "symbol") {
|
|
5841
|
+
recordKeys.add(typeof key === "number" ? key.toString() : key);
|
|
5842
|
+
const result = def.valueType._zod.run({
|
|
5843
|
+
value: input[key],
|
|
5844
|
+
issues: []
|
|
5845
|
+
}, ctx);
|
|
5846
|
+
if (result instanceof Promise) proms.push(result.then((result$1) => {
|
|
5847
|
+
if (result$1.issues.length) payload.issues.push(...prefixIssues(key, result$1.issues));
|
|
5848
|
+
payload.value[key] = result$1.value;
|
|
5849
|
+
}));
|
|
5850
|
+
else {
|
|
5851
|
+
if (result.issues.length) payload.issues.push(...prefixIssues(key, result.issues));
|
|
5852
|
+
payload.value[key] = result.value;
|
|
5853
|
+
}
|
|
5854
|
+
}
|
|
5855
|
+
let unrecognized;
|
|
5856
|
+
for (const key in input) if (!recordKeys.has(key)) {
|
|
5857
|
+
unrecognized = unrecognized ?? [];
|
|
5858
|
+
unrecognized.push(key);
|
|
5859
|
+
}
|
|
5860
|
+
if (unrecognized && unrecognized.length > 0) payload.issues.push({
|
|
5861
|
+
code: "unrecognized_keys",
|
|
5862
|
+
input,
|
|
5863
|
+
inst,
|
|
5864
|
+
keys: unrecognized
|
|
5865
|
+
});
|
|
5866
|
+
} else {
|
|
5867
|
+
payload.value = {};
|
|
5868
|
+
for (const key of Reflect.ownKeys(input)) {
|
|
5869
|
+
if (key === "__proto__") continue;
|
|
5870
|
+
let keyResult = def.keyType._zod.run({
|
|
5871
|
+
value: key,
|
|
5872
|
+
issues: []
|
|
5873
|
+
}, ctx);
|
|
5874
|
+
if (keyResult instanceof Promise) throw new Error("Async schemas not supported in object keys currently");
|
|
5875
|
+
if (typeof key === "string" && number$1.test(key) && keyResult.issues.length && keyResult.issues.some((iss) => iss.code === "invalid_type" && iss.expected === "number")) {
|
|
5876
|
+
const retryResult = def.keyType._zod.run({
|
|
5877
|
+
value: Number(key),
|
|
5878
|
+
issues: []
|
|
5879
|
+
}, ctx);
|
|
5880
|
+
if (retryResult instanceof Promise) throw new Error("Async schemas not supported in object keys currently");
|
|
5881
|
+
if (retryResult.issues.length === 0) keyResult = retryResult;
|
|
5882
|
+
}
|
|
5883
|
+
if (keyResult.issues.length) {
|
|
5884
|
+
if (def.mode === "loose") payload.value[key] = input[key];
|
|
5885
|
+
else payload.issues.push({
|
|
5886
|
+
code: "invalid_key",
|
|
5887
|
+
origin: "record",
|
|
5888
|
+
issues: keyResult.issues.map((iss) => finalizeIssue(iss, ctx, config())),
|
|
5889
|
+
input: key,
|
|
5890
|
+
path: [key],
|
|
5891
|
+
inst
|
|
5892
|
+
});
|
|
5893
|
+
continue;
|
|
5894
|
+
}
|
|
5895
|
+
const result = def.valueType._zod.run({
|
|
5896
|
+
value: input[key],
|
|
5897
|
+
issues: []
|
|
5898
|
+
}, ctx);
|
|
5899
|
+
if (result instanceof Promise) proms.push(result.then((result$1) => {
|
|
5900
|
+
if (result$1.issues.length) payload.issues.push(...prefixIssues(key, result$1.issues));
|
|
5901
|
+
payload.value[keyResult.value] = result$1.value;
|
|
5902
|
+
}));
|
|
5903
|
+
else {
|
|
5904
|
+
if (result.issues.length) payload.issues.push(...prefixIssues(key, result.issues));
|
|
5905
|
+
payload.value[keyResult.value] = result.value;
|
|
5906
|
+
}
|
|
5907
|
+
}
|
|
5908
|
+
}
|
|
5909
|
+
if (proms.length) return Promise.all(proms).then(() => payload);
|
|
5910
|
+
return payload;
|
|
5911
|
+
};
|
|
5912
|
+
});
|
|
5822
5913
|
const $ZodEnum = /* @__PURE__ */ $constructor("$ZodEnum", (inst, def) => {
|
|
5823
5914
|
$ZodType.init(inst, def);
|
|
5824
5915
|
const values = getEnumValues(def.entries);
|
|
@@ -7109,6 +7200,39 @@ const tupleProcessor = (schema, ctx, _json, params) => {
|
|
|
7109
7200
|
if (typeof minimum === "number") json.minItems = minimum;
|
|
7110
7201
|
if (typeof maximum === "number") json.maxItems = maximum;
|
|
7111
7202
|
};
|
|
7203
|
+
const recordProcessor = (schema, ctx, _json, params) => {
|
|
7204
|
+
const json = _json;
|
|
7205
|
+
const def = schema._zod.def;
|
|
7206
|
+
json.type = "object";
|
|
7207
|
+
const keyType = def.keyType;
|
|
7208
|
+
const patterns = keyType._zod.bag?.patterns;
|
|
7209
|
+
if (def.mode === "loose" && patterns && patterns.size > 0) {
|
|
7210
|
+
const valueSchema = process$2(def.valueType, ctx, {
|
|
7211
|
+
...params,
|
|
7212
|
+
path: [
|
|
7213
|
+
...params.path,
|
|
7214
|
+
"patternProperties",
|
|
7215
|
+
"*"
|
|
7216
|
+
]
|
|
7217
|
+
});
|
|
7218
|
+
json.patternProperties = {};
|
|
7219
|
+
for (const pattern of patterns) json.patternProperties[pattern.source] = valueSchema;
|
|
7220
|
+
} else {
|
|
7221
|
+
if (ctx.target === "draft-07" || ctx.target === "draft-2020-12") json.propertyNames = process$2(def.keyType, ctx, {
|
|
7222
|
+
...params,
|
|
7223
|
+
path: [...params.path, "propertyNames"]
|
|
7224
|
+
});
|
|
7225
|
+
json.additionalProperties = process$2(def.valueType, ctx, {
|
|
7226
|
+
...params,
|
|
7227
|
+
path: [...params.path, "additionalProperties"]
|
|
7228
|
+
});
|
|
7229
|
+
}
|
|
7230
|
+
const keyValues = keyType._zod.values;
|
|
7231
|
+
if (keyValues) {
|
|
7232
|
+
const validKeyValues = [...keyValues].filter((v$1) => typeof v$1 === "string" || typeof v$1 === "number");
|
|
7233
|
+
if (validKeyValues.length > 0) json.required = validKeyValues;
|
|
7234
|
+
}
|
|
7235
|
+
};
|
|
7112
7236
|
const nullableProcessor = (schema, ctx, json, params) => {
|
|
7113
7237
|
const def = schema._zod.def;
|
|
7114
7238
|
const inner = process$2(def.innerType, ctx, params);
|
|
@@ -7638,6 +7762,21 @@ function tuple(items, _paramsOrRest, _params) {
|
|
|
7638
7762
|
...normalizeParams(params)
|
|
7639
7763
|
});
|
|
7640
7764
|
}
|
|
7765
|
+
const ZodRecord = /* @__PURE__ */ $constructor("ZodRecord", (inst, def) => {
|
|
7766
|
+
$ZodRecord.init(inst, def);
|
|
7767
|
+
ZodType.init(inst, def);
|
|
7768
|
+
inst._zod.processJSONSchema = (ctx, json, params) => recordProcessor(inst, ctx, json, params);
|
|
7769
|
+
inst.keyType = def.keyType;
|
|
7770
|
+
inst.valueType = def.valueType;
|
|
7771
|
+
});
|
|
7772
|
+
function record(keyType, valueType, params) {
|
|
7773
|
+
return new ZodRecord({
|
|
7774
|
+
type: "record",
|
|
7775
|
+
keyType,
|
|
7776
|
+
valueType,
|
|
7777
|
+
...normalizeParams(params)
|
|
7778
|
+
});
|
|
7779
|
+
}
|
|
7641
7780
|
const ZodEnum = /* @__PURE__ */ $constructor("ZodEnum", (inst, def) => {
|
|
7642
7781
|
$ZodEnum.init(inst, def);
|
|
7643
7782
|
ZodType.init(inst, def);
|
|
@@ -39030,10 +39169,90 @@ function getIntegrationDisplayName(type) {
|
|
|
39030
39169
|
return type;
|
|
39031
39170
|
}
|
|
39032
39171
|
|
|
39172
|
+
//#endregion
|
|
39173
|
+
//#region src/core/connectors/config.ts
|
|
39174
|
+
/**
|
|
39175
|
+
* Schema for a single connector configuration
|
|
39176
|
+
*/
|
|
39177
|
+
const ConnectorConfigSchema = object({ scopes: array(string()).optional() });
|
|
39178
|
+
/**
|
|
39179
|
+
* Schema for the connectors.jsonc file
|
|
39180
|
+
*/
|
|
39181
|
+
const ConnectorsFileSchema = record(string(), ConnectorConfigSchema);
|
|
39182
|
+
const CONNECTORS_FILE_PATTERNS = [`${PROJECT_SUBDIR}/connectors.${CONFIG_FILE_EXTENSION_GLOB}`, `connectors.${CONFIG_FILE_EXTENSION_GLOB}`];
|
|
39183
|
+
/**
|
|
39184
|
+
* Find the connectors config file in the project
|
|
39185
|
+
*/
|
|
39186
|
+
async function findConnectorsFile(startPath) {
|
|
39187
|
+
return (await globby(CONNECTORS_FILE_PATTERNS, {
|
|
39188
|
+
cwd: startPath || process.cwd(),
|
|
39189
|
+
absolute: true
|
|
39190
|
+
}))[0] ?? null;
|
|
39191
|
+
}
|
|
39192
|
+
/**
|
|
39193
|
+
* Get the default path for the connectors file
|
|
39194
|
+
*/
|
|
39195
|
+
function getDefaultConnectorsPath(projectRoot) {
|
|
39196
|
+
return join(projectRoot || process.cwd(), PROJECT_SUBDIR, "connectors.jsonc");
|
|
39197
|
+
}
|
|
39198
|
+
/**
|
|
39199
|
+
* Read all connectors from the local config file
|
|
39200
|
+
*/
|
|
39201
|
+
async function readLocalConnectors(projectRoot) {
|
|
39202
|
+
const filePath = await findConnectorsFile(projectRoot);
|
|
39203
|
+
if (!filePath) return [];
|
|
39204
|
+
const parsed = await readJsonFile(filePath);
|
|
39205
|
+
const result = ConnectorsFileSchema.safeParse(parsed);
|
|
39206
|
+
if (!result.success) throw new Error(`Invalid connectors configuration: ${result.error.message}`);
|
|
39207
|
+
const connectors = [];
|
|
39208
|
+
for (const [type, config$1] of Object.entries(result.data)) {
|
|
39209
|
+
if (!isValidIntegration(type)) throw new Error(`Unknown connector type: ${type}`);
|
|
39210
|
+
connectors.push({
|
|
39211
|
+
type,
|
|
39212
|
+
scopes: config$1.scopes
|
|
39213
|
+
});
|
|
39214
|
+
}
|
|
39215
|
+
return connectors;
|
|
39216
|
+
}
|
|
39217
|
+
/**
|
|
39218
|
+
* Write connectors to the local config file
|
|
39219
|
+
*/
|
|
39220
|
+
async function writeLocalConnectors(connectors, projectRoot) {
|
|
39221
|
+
let filePath = await findConnectorsFile(projectRoot);
|
|
39222
|
+
if (!filePath) filePath = getDefaultConnectorsPath(projectRoot);
|
|
39223
|
+
const data = {};
|
|
39224
|
+
for (const connector of connectors) data[connector.type] = { ...connector.scopes && { scopes: connector.scopes } };
|
|
39225
|
+
await writeJsonFile(filePath, data);
|
|
39226
|
+
return filePath;
|
|
39227
|
+
}
|
|
39228
|
+
/**
|
|
39229
|
+
* Add a connector to the local config file
|
|
39230
|
+
*/
|
|
39231
|
+
async function addLocalConnector(type, scopes, projectRoot) {
|
|
39232
|
+
const connectors = await readLocalConnectors(projectRoot);
|
|
39233
|
+
const existing = connectors.find((c$1) => c$1.type === type);
|
|
39234
|
+
if (existing) {
|
|
39235
|
+
if (scopes) existing.scopes = scopes;
|
|
39236
|
+
} else connectors.push({
|
|
39237
|
+
type,
|
|
39238
|
+
scopes
|
|
39239
|
+
});
|
|
39240
|
+
return await writeLocalConnectors(connectors, projectRoot);
|
|
39241
|
+
}
|
|
39242
|
+
/**
|
|
39243
|
+
* Remove a connector from the local config file
|
|
39244
|
+
*/
|
|
39245
|
+
async function removeLocalConnector(type, projectRoot) {
|
|
39246
|
+
const connectors = await readLocalConnectors(projectRoot);
|
|
39247
|
+
const filtered = connectors.filter((c$1) => c$1.type !== type);
|
|
39248
|
+
if (filtered.length === connectors.length) return null;
|
|
39249
|
+
return await writeLocalConnectors(filtered, projectRoot);
|
|
39250
|
+
}
|
|
39251
|
+
|
|
39033
39252
|
//#endregion
|
|
39034
39253
|
//#region src/cli/commands/connectors/add.ts
|
|
39035
|
-
const POLL_INTERVAL_MS = 2e3;
|
|
39036
|
-
const POLL_TIMEOUT_MS = 300 * 1e3;
|
|
39254
|
+
const POLL_INTERVAL_MS$1 = 2e3;
|
|
39255
|
+
const POLL_TIMEOUT_MS$1 = 300 * 1e3;
|
|
39037
39256
|
async function promptForIntegrationType() {
|
|
39038
39257
|
const selected = await ve({
|
|
39039
39258
|
message: "Select an integration to connect:",
|
|
@@ -39063,8 +39282,8 @@ async function waitForOAuthCompletion(integrationType, connectionId) {
|
|
|
39063
39282
|
updateMessage("Waiting for authorization in browser...");
|
|
39064
39283
|
return false;
|
|
39065
39284
|
}, {
|
|
39066
|
-
interval: POLL_INTERVAL_MS,
|
|
39067
|
-
timeout: POLL_TIMEOUT_MS
|
|
39285
|
+
interval: POLL_INTERVAL_MS$1,
|
|
39286
|
+
timeout: POLL_TIMEOUT_MS$1
|
|
39068
39287
|
});
|
|
39069
39288
|
}, {
|
|
39070
39289
|
successMessage: "Authorization completed!",
|
|
@@ -39105,13 +39324,17 @@ async function addConnector(integrationType) {
|
|
|
39105
39324
|
successMessage: `${displayName} OAuth initiated`,
|
|
39106
39325
|
errorMessage: `Failed to initiate ${displayName} connection`
|
|
39107
39326
|
});
|
|
39108
|
-
if (initiateResponse.already_authorized)
|
|
39327
|
+
if (initiateResponse.already_authorized) {
|
|
39328
|
+
await addLocalConnector(selectedType);
|
|
39329
|
+
return { outroMessage: `Already connected to ${theme.styles.bold(displayName)} (added to connectors.jsonc)` };
|
|
39330
|
+
}
|
|
39109
39331
|
if (initiateResponse.error === "different_user" && initiateResponse.other_user_email) throw new Error(`This app is already connected to ${displayName} by ${initiateResponse.other_user_email}`);
|
|
39110
39332
|
if (!initiateResponse.redirect_url || !initiateResponse.connection_id) throw new Error("Invalid response from server: missing redirect URL or connection ID");
|
|
39111
39333
|
M.info(`Opening browser for ${displayName} authorization...`);
|
|
39112
39334
|
await open_default(initiateResponse.redirect_url);
|
|
39113
39335
|
const result = await waitForOAuthCompletion(selectedType, initiateResponse.connection_id);
|
|
39114
39336
|
if (!result.success) throw new Error(result.error || "Authorization failed");
|
|
39337
|
+
await addLocalConnector(selectedType);
|
|
39115
39338
|
const accountInfo = result.accountEmail ? ` as ${theme.styles.bold(result.accountEmail)}` : "";
|
|
39116
39339
|
return { outroMessage: `Successfully connected to ${theme.styles.bold(displayName)}${accountInfo}` };
|
|
39117
39340
|
}
|
|
@@ -39124,28 +39347,78 @@ const connectorsAddCommand = new Command("connectors:add").argument("[type]", "I
|
|
|
39124
39347
|
|
|
39125
39348
|
//#endregion
|
|
39126
39349
|
//#region src/cli/commands/connectors/list.ts
|
|
39350
|
+
function mergeConnectors(local, backend) {
|
|
39351
|
+
const merged = /* @__PURE__ */ new Map();
|
|
39352
|
+
for (const connector of local) merged.set(connector.type, {
|
|
39353
|
+
type: connector.type,
|
|
39354
|
+
displayName: getIntegrationDisplayName(connector.type),
|
|
39355
|
+
inLocal: true,
|
|
39356
|
+
inBackend: false
|
|
39357
|
+
});
|
|
39358
|
+
for (const connector of backend) {
|
|
39359
|
+
const existing = merged.get(connector.integrationType);
|
|
39360
|
+
if (existing) {
|
|
39361
|
+
existing.inBackend = true;
|
|
39362
|
+
existing.status = connector.status;
|
|
39363
|
+
existing.accountEmail = connector.accountInfo?.email || connector.accountInfo?.name;
|
|
39364
|
+
} else merged.set(connector.integrationType, {
|
|
39365
|
+
type: connector.integrationType,
|
|
39366
|
+
displayName: getIntegrationDisplayName(connector.integrationType),
|
|
39367
|
+
inLocal: false,
|
|
39368
|
+
inBackend: true,
|
|
39369
|
+
status: connector.status,
|
|
39370
|
+
accountEmail: connector.accountInfo?.email || connector.accountInfo?.name
|
|
39371
|
+
});
|
|
39372
|
+
}
|
|
39373
|
+
return Array.from(merged.values());
|
|
39374
|
+
}
|
|
39127
39375
|
function formatConnectorLine(connector) {
|
|
39128
|
-
const
|
|
39129
|
-
const
|
|
39130
|
-
const
|
|
39131
|
-
|
|
39376
|
+
const { displayName, inLocal, inBackend, status, accountEmail } = connector;
|
|
39377
|
+
const isConnected$1 = inBackend && status?.toLowerCase() === "active";
|
|
39378
|
+
const isPending = inLocal && !inBackend;
|
|
39379
|
+
const isOrphaned = inBackend && !inLocal;
|
|
39380
|
+
let bullet;
|
|
39381
|
+
let statusText = "";
|
|
39382
|
+
if (isConnected$1) {
|
|
39383
|
+
bullet = theme.colors.success("●");
|
|
39384
|
+
if (accountEmail) statusText = ` - ${accountEmail}`;
|
|
39385
|
+
} else if (isPending) {
|
|
39386
|
+
bullet = theme.colors.warning("○");
|
|
39387
|
+
statusText = theme.styles.dim(" (not connected)");
|
|
39388
|
+
} else if (isOrphaned) {
|
|
39389
|
+
bullet = theme.colors.error("○");
|
|
39390
|
+
statusText = theme.styles.dim(" (not in local config)");
|
|
39391
|
+
} else {
|
|
39392
|
+
bullet = theme.colors.error("○");
|
|
39393
|
+
statusText = theme.styles.dim(` (${status || "disconnected"})`);
|
|
39394
|
+
}
|
|
39395
|
+
return `${bullet} ${displayName}${statusText}`;
|
|
39132
39396
|
}
|
|
39133
39397
|
async function listConnectorsCommand() {
|
|
39134
|
-
const
|
|
39135
|
-
|
|
39398
|
+
const [localConnectors, backendConnectors] = await runTask("Fetching connectors...", async () => {
|
|
39399
|
+
const [local, backend] = await Promise.all([readLocalConnectors().catch(() => []), listConnectors().catch(() => [])]);
|
|
39400
|
+
return [local, backend];
|
|
39136
39401
|
}, {
|
|
39137
39402
|
successMessage: "Connectors loaded",
|
|
39138
39403
|
errorMessage: "Failed to fetch connectors"
|
|
39139
39404
|
});
|
|
39140
|
-
|
|
39405
|
+
const merged = mergeConnectors(localConnectors, backendConnectors);
|
|
39406
|
+
if (merged.length === 0) {
|
|
39141
39407
|
M.info("No connectors configured for this app.");
|
|
39142
39408
|
M.info(`Run ${theme.styles.bold("base44 connectors:add")} to connect an integration.`);
|
|
39143
39409
|
return { outroMessage: "" };
|
|
39144
39410
|
}
|
|
39145
39411
|
console.log();
|
|
39146
|
-
for (const connector of
|
|
39412
|
+
for (const connector of merged) console.log(formatConnectorLine(connector));
|
|
39147
39413
|
console.log();
|
|
39148
|
-
|
|
39414
|
+
const connected = merged.filter((c$1) => c$1.inBackend && c$1.status?.toLowerCase() === "active").length;
|
|
39415
|
+
const pending = merged.filter((c$1) => c$1.inLocal && !c$1.inBackend).length;
|
|
39416
|
+
let summary = `${connected} connected`;
|
|
39417
|
+
if (pending > 0) {
|
|
39418
|
+
summary += `, ${pending} pending`;
|
|
39419
|
+
M.info(`Run ${theme.styles.bold("base44 connectors:push")} to connect pending integrations.`);
|
|
39420
|
+
}
|
|
39421
|
+
return { outroMessage: summary };
|
|
39149
39422
|
}
|
|
39150
39423
|
const connectorsListCommand = new Command("connectors:list").description("List all connected OAuth integrations").action(async () => {
|
|
39151
39424
|
await runCommand(listConnectorsCommand, {
|
|
@@ -39154,56 +39427,195 @@ const connectorsListCommand = new Command("connectors:list").description("List a
|
|
|
39154
39427
|
});
|
|
39155
39428
|
});
|
|
39156
39429
|
|
|
39430
|
+
//#endregion
|
|
39431
|
+
//#region src/cli/commands/connectors/push.ts
|
|
39432
|
+
const POLL_INTERVAL_MS = 2e3;
|
|
39433
|
+
const POLL_TIMEOUT_MS = 300 * 1e3;
|
|
39434
|
+
function findPendingConnectors(local, backend) {
|
|
39435
|
+
const connectedTypes = new Set(backend.filter((c$1) => c$1.status.toLowerCase() === "active").map((c$1) => c$1.integrationType));
|
|
39436
|
+
return local.filter((c$1) => !connectedTypes.has(c$1.type)).map((c$1) => ({
|
|
39437
|
+
type: c$1.type,
|
|
39438
|
+
displayName: getIntegrationDisplayName(c$1.type),
|
|
39439
|
+
scopes: c$1.scopes
|
|
39440
|
+
}));
|
|
39441
|
+
}
|
|
39442
|
+
async function connectSingleConnector(connector) {
|
|
39443
|
+
const { type, displayName, scopes } = connector;
|
|
39444
|
+
const initiateResponse = await initiateOAuth(type, scopes || null);
|
|
39445
|
+
if (initiateResponse.already_authorized) return { success: true };
|
|
39446
|
+
if (initiateResponse.error === "different_user") return {
|
|
39447
|
+
success: false,
|
|
39448
|
+
error: `Already connected by ${initiateResponse.other_user_email}`
|
|
39449
|
+
};
|
|
39450
|
+
if (!initiateResponse.redirect_url || !initiateResponse.connection_id) return {
|
|
39451
|
+
success: false,
|
|
39452
|
+
error: "Invalid response from server"
|
|
39453
|
+
};
|
|
39454
|
+
M.info(`Opening browser for ${displayName} authorization...`);
|
|
39455
|
+
await open_default(initiateResponse.redirect_url);
|
|
39456
|
+
let accountEmail;
|
|
39457
|
+
try {
|
|
39458
|
+
await pWaitFor(async () => {
|
|
39459
|
+
const status = await checkOAuthStatus(type, initiateResponse.connection_id);
|
|
39460
|
+
if (status.status === "ACTIVE") {
|
|
39461
|
+
accountEmail = status.accountEmail;
|
|
39462
|
+
return true;
|
|
39463
|
+
}
|
|
39464
|
+
if (status.status === "FAILED") throw new Error(status.error || "Authorization failed");
|
|
39465
|
+
return false;
|
|
39466
|
+
}, {
|
|
39467
|
+
interval: POLL_INTERVAL_MS,
|
|
39468
|
+
timeout: POLL_TIMEOUT_MS
|
|
39469
|
+
});
|
|
39470
|
+
return {
|
|
39471
|
+
success: true,
|
|
39472
|
+
accountEmail
|
|
39473
|
+
};
|
|
39474
|
+
} catch (err) {
|
|
39475
|
+
if (err instanceof Error && err.message.includes("timed out")) return {
|
|
39476
|
+
success: false,
|
|
39477
|
+
error: "Authorization timed out"
|
|
39478
|
+
};
|
|
39479
|
+
return {
|
|
39480
|
+
success: false,
|
|
39481
|
+
error: err instanceof Error ? err.message : "Unknown error"
|
|
39482
|
+
};
|
|
39483
|
+
}
|
|
39484
|
+
}
|
|
39485
|
+
async function pushConnectorsCommand() {
|
|
39486
|
+
const [localConnectors, backendConnectors] = await runTask("Checking connector status...", async () => {
|
|
39487
|
+
const [local, backend] = await Promise.all([readLocalConnectors(), listConnectors().catch(() => [])]);
|
|
39488
|
+
return [local, backend];
|
|
39489
|
+
}, {
|
|
39490
|
+
successMessage: "Status checked",
|
|
39491
|
+
errorMessage: "Failed to check status"
|
|
39492
|
+
});
|
|
39493
|
+
if (localConnectors.length === 0) {
|
|
39494
|
+
M.info("No connectors defined in connectors.jsonc");
|
|
39495
|
+
M.info(`Run ${theme.styles.bold("base44 connectors:add")} to add a connector.`);
|
|
39496
|
+
return { outroMessage: "" };
|
|
39497
|
+
}
|
|
39498
|
+
const pending = findPendingConnectors(localConnectors, backendConnectors);
|
|
39499
|
+
if (pending.length === 0) return { outroMessage: "All connectors are already connected" };
|
|
39500
|
+
console.log();
|
|
39501
|
+
M.info(`${pending.length} connector${pending.length === 1 ? "" : "s"} need${pending.length === 1 ? "s" : ""} to be connected:`);
|
|
39502
|
+
for (const c$1 of pending) console.log(` ${theme.colors.warning("○")} ${c$1.displayName}`);
|
|
39503
|
+
console.log();
|
|
39504
|
+
const shouldProceed = await ye({
|
|
39505
|
+
message: `Connect ${pending.length} integration${pending.length === 1 ? "" : "s"}?`,
|
|
39506
|
+
initialValue: true
|
|
39507
|
+
});
|
|
39508
|
+
if (pD(shouldProceed) || !shouldProceed) return { outroMessage: "Cancelled" };
|
|
39509
|
+
let connected = 0;
|
|
39510
|
+
let failed = 0;
|
|
39511
|
+
for (const connector of pending) {
|
|
39512
|
+
console.log();
|
|
39513
|
+
M.info(`Connecting ${theme.styles.bold(connector.displayName)}...`);
|
|
39514
|
+
const result = await connectSingleConnector(connector);
|
|
39515
|
+
if (result.success) {
|
|
39516
|
+
const accountInfo = result.accountEmail ? ` as ${result.accountEmail}` : "";
|
|
39517
|
+
M.success(`${connector.displayName} connected${accountInfo}`);
|
|
39518
|
+
connected++;
|
|
39519
|
+
} else {
|
|
39520
|
+
M.error(`${connector.displayName} failed: ${result.error}`);
|
|
39521
|
+
failed++;
|
|
39522
|
+
}
|
|
39523
|
+
}
|
|
39524
|
+
console.log();
|
|
39525
|
+
if (failed === 0) return { outroMessage: `Successfully connected ${connected} integration${connected === 1 ? "" : "s"}` };
|
|
39526
|
+
return { outroMessage: `Connected ${connected}, failed ${failed}` };
|
|
39527
|
+
}
|
|
39528
|
+
const connectorsPushCommand = new Command("connectors:push").description("Connect all pending integrations from connectors.jsonc").action(async () => {
|
|
39529
|
+
await runCommand(pushConnectorsCommand, {
|
|
39530
|
+
requireAuth: true,
|
|
39531
|
+
requireAppConfig: true
|
|
39532
|
+
});
|
|
39533
|
+
});
|
|
39534
|
+
|
|
39157
39535
|
//#endregion
|
|
39158
39536
|
//#region src/cli/commands/connectors/remove.ts
|
|
39537
|
+
function mergeConnectorsForRemoval(local, backend) {
|
|
39538
|
+
const merged = /* @__PURE__ */ new Map();
|
|
39539
|
+
for (const connector of local) merged.set(connector.type, {
|
|
39540
|
+
type: connector.type,
|
|
39541
|
+
displayName: getIntegrationDisplayName(connector.type),
|
|
39542
|
+
inLocal: true,
|
|
39543
|
+
inBackend: false
|
|
39544
|
+
});
|
|
39545
|
+
for (const connector of backend) {
|
|
39546
|
+
if (!isValidIntegration(connector.integrationType)) continue;
|
|
39547
|
+
const existing = merged.get(connector.integrationType);
|
|
39548
|
+
if (existing) {
|
|
39549
|
+
existing.inBackend = true;
|
|
39550
|
+
existing.accountEmail = connector.accountInfo?.email || connector.accountInfo?.name;
|
|
39551
|
+
} else merged.set(connector.integrationType, {
|
|
39552
|
+
type: connector.integrationType,
|
|
39553
|
+
displayName: getIntegrationDisplayName(connector.integrationType),
|
|
39554
|
+
inLocal: false,
|
|
39555
|
+
inBackend: true,
|
|
39556
|
+
accountEmail: connector.accountInfo?.email || connector.accountInfo?.name
|
|
39557
|
+
});
|
|
39558
|
+
}
|
|
39559
|
+
return Array.from(merged.values());
|
|
39560
|
+
}
|
|
39159
39561
|
async function promptForConnectorToRemove(connectors) {
|
|
39160
39562
|
const selected = await ve({
|
|
39161
39563
|
message: "Select a connector to remove:",
|
|
39162
|
-
options: connectors.map((c$1) =>
|
|
39163
|
-
|
|
39164
|
-
|
|
39165
|
-
|
|
39564
|
+
options: connectors.map((c$1) => {
|
|
39565
|
+
let label = c$1.displayName;
|
|
39566
|
+
if (c$1.accountEmail) label += ` (${c$1.accountEmail})`;
|
|
39567
|
+
else if (c$1.inLocal && !c$1.inBackend) label += " (not connected)";
|
|
39568
|
+
return {
|
|
39569
|
+
value: c$1.type,
|
|
39570
|
+
label
|
|
39571
|
+
};
|
|
39572
|
+
})
|
|
39166
39573
|
});
|
|
39167
39574
|
if (pD(selected)) return null;
|
|
39168
39575
|
return selected;
|
|
39169
39576
|
}
|
|
39170
39577
|
async function removeConnectorCommand(integrationType, options = {}) {
|
|
39171
39578
|
const isHardDelete = options.hard === true;
|
|
39172
|
-
const
|
|
39173
|
-
|
|
39579
|
+
const [localConnectors, backendConnectors] = await runTask("Fetching connectors...", async () => {
|
|
39580
|
+
const [local, backend] = await Promise.all([readLocalConnectors().catch(() => []), listConnectors().catch(() => [])]);
|
|
39581
|
+
return [local, backend];
|
|
39174
39582
|
}, {
|
|
39175
39583
|
successMessage: "Connectors loaded",
|
|
39176
39584
|
errorMessage: "Failed to fetch connectors"
|
|
39177
39585
|
});
|
|
39178
|
-
|
|
39586
|
+
const merged = mergeConnectorsForRemoval(localConnectors, backendConnectors);
|
|
39587
|
+
if (merged.length === 0) return { outroMessage: "No connectors to remove" };
|
|
39179
39588
|
let selectedType;
|
|
39589
|
+
let selectedConnector;
|
|
39180
39590
|
if (!integrationType) {
|
|
39181
|
-
const prompted = await promptForConnectorToRemove(
|
|
39591
|
+
const prompted = await promptForConnectorToRemove(merged);
|
|
39182
39592
|
if (!prompted) return { outroMessage: "Cancelled" };
|
|
39183
39593
|
selectedType = prompted;
|
|
39594
|
+
selectedConnector = merged.find((c$1) => c$1.type === selectedType);
|
|
39184
39595
|
} else {
|
|
39185
39596
|
if (!isValidIntegration(integrationType)) throw new Error(`Invalid connector type: ${integrationType}`);
|
|
39186
|
-
|
|
39597
|
+
selectedConnector = merged.find((c$1) => c$1.type === integrationType);
|
|
39598
|
+
if (!selectedConnector) throw new Error(`No ${getIntegrationDisplayName(integrationType)} connector found`);
|
|
39187
39599
|
selectedType = integrationType;
|
|
39188
39600
|
}
|
|
39189
39601
|
const displayName = getIntegrationDisplayName(selectedType);
|
|
39190
|
-
const
|
|
39191
|
-
const accountInfo = connector?.accountInfo?.email ? ` (${connector.accountInfo.email})` : "";
|
|
39602
|
+
const accountInfo = selectedConnector?.accountEmail ? ` (${selectedConnector.accountEmail})` : "";
|
|
39192
39603
|
const shouldRemove = await ye({
|
|
39193
|
-
message: `${isHardDelete ? "Permanently remove" : "
|
|
39604
|
+
message: `${isHardDelete ? "Permanently remove" : "Remove"} ${displayName}${accountInfo}?`,
|
|
39194
39605
|
initialValue: false
|
|
39195
39606
|
});
|
|
39196
39607
|
if (pD(shouldRemove) || !shouldRemove) return { outroMessage: "Cancelled" };
|
|
39197
|
-
await runTask(isHardDelete ? `Removing ${displayName}...` : `
|
|
39198
|
-
if (isHardDelete) await removeConnector(selectedType);
|
|
39608
|
+
await runTask(isHardDelete ? `Removing ${displayName}...` : `Removing ${displayName}...`, async () => {
|
|
39609
|
+
if (selectedConnector?.inBackend) if (isHardDelete) await removeConnector(selectedType);
|
|
39199
39610
|
else await disconnectConnector(selectedType);
|
|
39611
|
+
await removeLocalConnector(selectedType);
|
|
39200
39612
|
}, {
|
|
39201
|
-
successMessage:
|
|
39202
|
-
errorMessage:
|
|
39613
|
+
successMessage: `${displayName} removed`,
|
|
39614
|
+
errorMessage: `Failed to remove ${displayName}`
|
|
39203
39615
|
});
|
|
39204
|
-
return { outroMessage: `Successfully
|
|
39616
|
+
return { outroMessage: `Successfully removed ${theme.styles.bold(displayName)}` };
|
|
39205
39617
|
}
|
|
39206
|
-
const connectorsRemoveCommand = new Command("connectors:remove").argument("[type]", "Integration type to remove (e.g., slack, notion)").option("--hard", "Permanently remove the connector (cannot be undone)").description("
|
|
39618
|
+
const connectorsRemoveCommand = new Command("connectors:remove").argument("[type]", "Integration type to remove (e.g., slack, notion)").option("--hard", "Permanently remove the connector (cannot be undone)").description("Remove an OAuth integration").action(async (type, options) => {
|
|
39207
39619
|
await runCommand(() => removeConnectorCommand(type, options), {
|
|
39208
39620
|
requireAuth: true,
|
|
39209
39621
|
requireAppConfig: true
|
|
@@ -39231,6 +39643,7 @@ program.addCommand(functionsDeployCommand);
|
|
|
39231
39643
|
program.addCommand(siteDeployCommand);
|
|
39232
39644
|
program.addCommand(connectorsAddCommand);
|
|
39233
39645
|
program.addCommand(connectorsListCommand);
|
|
39646
|
+
program.addCommand(connectorsPushCommand);
|
|
39234
39647
|
program.addCommand(connectorsRemoveCommand);
|
|
39235
39648
|
program.parse();
|
|
39236
39649
|
|
package/package.json
CHANGED