@base44-preview/cli 0.0.15-pr.99.76064e9 → 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 +46 -0
- package/dist/cli/index.js +460 -67
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -70,6 +70,52 @@ base44 deploy
|
|
|
70
70
|
|---------|-------------|
|
|
71
71
|
| `base44 site deploy` | Deploy built site files to Base44 hosting |
|
|
72
72
|
|
|
73
|
+
### Connectors
|
|
74
|
+
|
|
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
|
+
|
|
77
|
+
| Command | Description |
|
|
78
|
+
|---------|-------------|
|
|
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 |
|
|
83
|
+
| `base44 connectors:remove [type] --hard` | Permanently remove an integration |
|
|
84
|
+
|
|
85
|
+
**Supported integrations:** Slack, Google Calendar, Google Drive, Gmail, Google Sheets, Google Docs, Google Slides, Notion, Salesforce, HubSpot, LinkedIn, TikTok
|
|
86
|
+
|
|
87
|
+
**Example workflow:**
|
|
88
|
+
```bash
|
|
89
|
+
# Add connectors (saves to connectors.jsonc and opens OAuth)
|
|
90
|
+
base44 connectors:add slack
|
|
91
|
+
base44 connectors:add googlecalendar
|
|
92
|
+
|
|
93
|
+
# List connectors showing local vs connected status
|
|
94
|
+
base44 connectors:list
|
|
95
|
+
# Output:
|
|
96
|
+
# ● Slack - user@example.com
|
|
97
|
+
# ○ Google Calendar (not connected)
|
|
98
|
+
|
|
99
|
+
# Connect all pending integrations
|
|
100
|
+
base44 connectors:push
|
|
101
|
+
|
|
102
|
+
# Remove a connector
|
|
103
|
+
base44 connectors:remove slack
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Local configuration** (`base44/connectors.jsonc`):
|
|
107
|
+
```jsonc
|
|
108
|
+
{
|
|
109
|
+
"slack": {},
|
|
110
|
+
"googlecalendar": { "scopes": ["calendar.readonly"] }
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Once connected, use the SDK's `connectors.getAccessToken()` to retrieve tokens:
|
|
115
|
+
```javascript
|
|
116
|
+
const token = await base44.connectors.getAccessToken("slack");
|
|
117
|
+
```
|
|
118
|
+
|
|
73
119
|
## Configuration
|
|
74
120
|
|
|
75
121
|
### Project Configuration
|
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);
|
|
@@ -38971,6 +39110,19 @@ async function disconnectConnector(integrationType) {
|
|
|
38971
39110
|
throw new ConnectorApiError(`Failed to disconnect connector: ${response.status} ${response.statusText}`);
|
|
38972
39111
|
}
|
|
38973
39112
|
}
|
|
39113
|
+
/**
|
|
39114
|
+
* Removes (hard delete) a connector integration.
|
|
39115
|
+
* This permanently removes the connector and cannot be undone.
|
|
39116
|
+
*/
|
|
39117
|
+
async function removeConnector(integrationType) {
|
|
39118
|
+
const response = await getAppClient().delete(`external-auth/integrations/${integrationType}/remove`, { throwHttpErrors: false });
|
|
39119
|
+
if (!response.ok) {
|
|
39120
|
+
const json = await response.json();
|
|
39121
|
+
const errorResult = ApiErrorSchema.safeParse(json);
|
|
39122
|
+
if (errorResult.success) throw new ConnectorApiError(errorResult.data.error);
|
|
39123
|
+
throw new ConnectorApiError(`Failed to remove connector: ${response.status} ${response.statusText}`);
|
|
39124
|
+
}
|
|
39125
|
+
}
|
|
38974
39126
|
|
|
38975
39127
|
//#endregion
|
|
38976
39128
|
//#region src/core/connectors/constants.ts
|
|
@@ -39017,10 +39169,90 @@ function getIntegrationDisplayName(type) {
|
|
|
39017
39169
|
return type;
|
|
39018
39170
|
}
|
|
39019
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
|
+
|
|
39020
39252
|
//#endregion
|
|
39021
39253
|
//#region src/cli/commands/connectors/add.ts
|
|
39022
|
-
const POLL_INTERVAL_MS = 2e3;
|
|
39023
|
-
const POLL_TIMEOUT_MS = 300 * 1e3;
|
|
39254
|
+
const POLL_INTERVAL_MS$1 = 2e3;
|
|
39255
|
+
const POLL_TIMEOUT_MS$1 = 300 * 1e3;
|
|
39024
39256
|
async function promptForIntegrationType() {
|
|
39025
39257
|
const selected = await ve({
|
|
39026
39258
|
message: "Select an integration to connect:",
|
|
@@ -39050,8 +39282,8 @@ async function waitForOAuthCompletion(integrationType, connectionId) {
|
|
|
39050
39282
|
updateMessage("Waiting for authorization in browser...");
|
|
39051
39283
|
return false;
|
|
39052
39284
|
}, {
|
|
39053
|
-
interval: POLL_INTERVAL_MS,
|
|
39054
|
-
timeout: POLL_TIMEOUT_MS
|
|
39285
|
+
interval: POLL_INTERVAL_MS$1,
|
|
39286
|
+
timeout: POLL_TIMEOUT_MS$1
|
|
39055
39287
|
});
|
|
39056
39288
|
}, {
|
|
39057
39289
|
successMessage: "Authorization completed!",
|
|
@@ -39092,13 +39324,17 @@ async function addConnector(integrationType) {
|
|
|
39092
39324
|
successMessage: `${displayName} OAuth initiated`,
|
|
39093
39325
|
errorMessage: `Failed to initiate ${displayName} connection`
|
|
39094
39326
|
});
|
|
39095
|
-
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
|
+
}
|
|
39096
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}`);
|
|
39097
39332
|
if (!initiateResponse.redirect_url || !initiateResponse.connection_id) throw new Error("Invalid response from server: missing redirect URL or connection ID");
|
|
39098
39333
|
M.info(`Opening browser for ${displayName} authorization...`);
|
|
39099
39334
|
await open_default(initiateResponse.redirect_url);
|
|
39100
39335
|
const result = await waitForOAuthCompletion(selectedType, initiateResponse.connection_id);
|
|
39101
39336
|
if (!result.success) throw new Error(result.error || "Authorization failed");
|
|
39337
|
+
await addLocalConnector(selectedType);
|
|
39102
39338
|
const accountInfo = result.accountEmail ? ` as ${theme.styles.bold(result.accountEmail)}` : "";
|
|
39103
39339
|
return { outroMessage: `Successfully connected to ${theme.styles.bold(displayName)}${accountInfo}` };
|
|
39104
39340
|
}
|
|
@@ -39111,64 +39347,78 @@ const connectorsAddCommand = new Command("connectors:add").argument("[type]", "I
|
|
|
39111
39347
|
|
|
39112
39348
|
//#endregion
|
|
39113
39349
|
//#region src/cli/commands/connectors/list.ts
|
|
39114
|
-
function
|
|
39115
|
-
|
|
39116
|
-
|
|
39117
|
-
|
|
39118
|
-
|
|
39119
|
-
|
|
39120
|
-
|
|
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
|
|
39121
39371
|
});
|
|
39122
|
-
} catch {
|
|
39123
|
-
return dateString;
|
|
39124
39372
|
}
|
|
39125
|
-
|
|
39126
|
-
|
|
39127
|
-
|
|
39128
|
-
|
|
39129
|
-
|
|
39130
|
-
|
|
39131
|
-
|
|
39373
|
+
return Array.from(merged.values());
|
|
39374
|
+
}
|
|
39375
|
+
function formatConnectorLine(connector) {
|
|
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
|
-
console.log(
|
|
39412
|
+
for (const connector of merged) console.log(formatConnectorLine(connector));
|
|
39147
39413
|
console.log();
|
|
39148
|
-
const
|
|
39149
|
-
|
|
39150
|
-
|
|
39151
|
-
|
|
39152
|
-
|
|
39153
|
-
|
|
39154
|
-
const colWidths = [
|
|
39155
|
-
20,
|
|
39156
|
-
30,
|
|
39157
|
-
15,
|
|
39158
|
-
15
|
|
39159
|
-
];
|
|
39160
|
-
const headerRow = headers.map((h$2, i$1) => h$2.padEnd(colWidths[i$1])).join(" ");
|
|
39161
|
-
console.log(theme.styles.dim(headerRow));
|
|
39162
|
-
console.log(theme.styles.dim("─".repeat(headerRow.length)));
|
|
39163
|
-
for (const connector of connectors) {
|
|
39164
|
-
const type = getIntegrationDisplayName(connector.integrationType).padEnd(colWidths[0]);
|
|
39165
|
-
const account = (connector.accountInfo?.email || connector.accountInfo?.name || "-").padEnd(colWidths[1]);
|
|
39166
|
-
const status = formatStatus(connector.status);
|
|
39167
|
-
const connected = formatDate(connector.connectedAt).padEnd(colWidths[3]);
|
|
39168
|
-
console.log(`${type} ${account} ${status.padEnd(colWidths[2] + 10)} ${connected}`);
|
|
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.`);
|
|
39169
39420
|
}
|
|
39170
|
-
|
|
39171
|
-
return { outroMessage: `${connectors.length} connector${connectors.length === 1 ? "" : "s"} configured` };
|
|
39421
|
+
return { outroMessage: summary };
|
|
39172
39422
|
}
|
|
39173
39423
|
const connectorsListCommand = new Command("connectors:list").description("List all connected OAuth integrations").action(async () => {
|
|
39174
39424
|
await runCommand(listConnectorsCommand, {
|
|
@@ -39177,54 +39427,196 @@ const connectorsListCommand = new Command("connectors:list").description("List a
|
|
|
39177
39427
|
});
|
|
39178
39428
|
});
|
|
39179
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
|
+
|
|
39180
39535
|
//#endregion
|
|
39181
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
|
+
}
|
|
39182
39561
|
async function promptForConnectorToRemove(connectors) {
|
|
39183
39562
|
const selected = await ve({
|
|
39184
39563
|
message: "Select a connector to remove:",
|
|
39185
|
-
options: connectors.map((c$1) =>
|
|
39186
|
-
|
|
39187
|
-
|
|
39188
|
-
|
|
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
|
+
})
|
|
39189
39573
|
});
|
|
39190
39574
|
if (pD(selected)) return null;
|
|
39191
39575
|
return selected;
|
|
39192
39576
|
}
|
|
39193
|
-
async function removeConnectorCommand(integrationType) {
|
|
39194
|
-
const
|
|
39195
|
-
|
|
39577
|
+
async function removeConnectorCommand(integrationType, options = {}) {
|
|
39578
|
+
const isHardDelete = options.hard === true;
|
|
39579
|
+
const [localConnectors, backendConnectors] = await runTask("Fetching connectors...", async () => {
|
|
39580
|
+
const [local, backend] = await Promise.all([readLocalConnectors().catch(() => []), listConnectors().catch(() => [])]);
|
|
39581
|
+
return [local, backend];
|
|
39196
39582
|
}, {
|
|
39197
39583
|
successMessage: "Connectors loaded",
|
|
39198
39584
|
errorMessage: "Failed to fetch connectors"
|
|
39199
39585
|
});
|
|
39200
|
-
|
|
39586
|
+
const merged = mergeConnectorsForRemoval(localConnectors, backendConnectors);
|
|
39587
|
+
if (merged.length === 0) return { outroMessage: "No connectors to remove" };
|
|
39201
39588
|
let selectedType;
|
|
39589
|
+
let selectedConnector;
|
|
39202
39590
|
if (!integrationType) {
|
|
39203
|
-
const prompted = await promptForConnectorToRemove(
|
|
39591
|
+
const prompted = await promptForConnectorToRemove(merged);
|
|
39204
39592
|
if (!prompted) return { outroMessage: "Cancelled" };
|
|
39205
39593
|
selectedType = prompted;
|
|
39594
|
+
selectedConnector = merged.find((c$1) => c$1.type === selectedType);
|
|
39206
39595
|
} else {
|
|
39207
39596
|
if (!isValidIntegration(integrationType)) throw new Error(`Invalid connector type: ${integrationType}`);
|
|
39208
|
-
|
|
39597
|
+
selectedConnector = merged.find((c$1) => c$1.type === integrationType);
|
|
39598
|
+
if (!selectedConnector) throw new Error(`No ${getIntegrationDisplayName(integrationType)} connector found`);
|
|
39209
39599
|
selectedType = integrationType;
|
|
39210
39600
|
}
|
|
39211
39601
|
const displayName = getIntegrationDisplayName(selectedType);
|
|
39212
|
-
const
|
|
39602
|
+
const accountInfo = selectedConnector?.accountEmail ? ` (${selectedConnector.accountEmail})` : "";
|
|
39213
39603
|
const shouldRemove = await ye({
|
|
39214
|
-
message:
|
|
39604
|
+
message: `${isHardDelete ? "Permanently remove" : "Remove"} ${displayName}${accountInfo}?`,
|
|
39215
39605
|
initialValue: false
|
|
39216
39606
|
});
|
|
39217
39607
|
if (pD(shouldRemove) || !shouldRemove) return { outroMessage: "Cancelled" };
|
|
39218
|
-
await runTask(`
|
|
39219
|
-
await
|
|
39608
|
+
await runTask(isHardDelete ? `Removing ${displayName}...` : `Removing ${displayName}...`, async () => {
|
|
39609
|
+
if (selectedConnector?.inBackend) if (isHardDelete) await removeConnector(selectedType);
|
|
39610
|
+
else await disconnectConnector(selectedType);
|
|
39611
|
+
await removeLocalConnector(selectedType);
|
|
39220
39612
|
}, {
|
|
39221
|
-
successMessage: `${displayName}
|
|
39222
|
-
errorMessage: `Failed to
|
|
39613
|
+
successMessage: `${displayName} removed`,
|
|
39614
|
+
errorMessage: `Failed to remove ${displayName}`
|
|
39223
39615
|
});
|
|
39224
|
-
return { outroMessage: `Successfully
|
|
39616
|
+
return { outroMessage: `Successfully removed ${theme.styles.bold(displayName)}` };
|
|
39225
39617
|
}
|
|
39226
|
-
const connectorsRemoveCommand = new Command("connectors:remove").argument("[type]", "Integration type to remove (e.g., slack, notion)").description("
|
|
39227
|
-
await runCommand(() => removeConnectorCommand(type), {
|
|
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) => {
|
|
39619
|
+
await runCommand(() => removeConnectorCommand(type, options), {
|
|
39228
39620
|
requireAuth: true,
|
|
39229
39621
|
requireAppConfig: true
|
|
39230
39622
|
});
|
|
@@ -39251,6 +39643,7 @@ program.addCommand(functionsDeployCommand);
|
|
|
39251
39643
|
program.addCommand(siteDeployCommand);
|
|
39252
39644
|
program.addCommand(connectorsAddCommand);
|
|
39253
39645
|
program.addCommand(connectorsListCommand);
|
|
39646
|
+
program.addCommand(connectorsPushCommand);
|
|
39254
39647
|
program.addCommand(connectorsRemoveCommand);
|
|
39255
39648
|
program.parse();
|
|
39256
39649
|
|
package/package.json
CHANGED