@base44-preview/cli 0.0.15-pr.102.ec2ad29 → 0.0.15-pr.104.996eeac
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 +40 -78
- package/dist/cli/index.js +726 -328
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -70,91 +70,58 @@ base44 deploy
|
|
|
70
70
|
|---------|-------------|
|
|
71
71
|
| `base44 site deploy` | Deploy built site files to Base44 hosting |
|
|
72
72
|
|
|
73
|
-
###
|
|
73
|
+
### Connectors
|
|
74
|
+
|
|
75
|
+
Manage OAuth integrations to connect your app with external services. Connectors are stored in the `connectors` property of your project's `config.jsonc` file.
|
|
74
76
|
|
|
75
77
|
| Command | Description |
|
|
76
78
|
|---------|-------------|
|
|
77
|
-
| `base44
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
## TypeScript Type Generation
|
|
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` | Sync connectors with backend (connect new, remove missing) |
|
|
82
|
+
| `base44 connectors remove [type]` | Remove an integration |
|
|
83
|
+
| `base44 connectors remove [type] --hard` | Permanently remove an integration |
|
|
84
84
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
### Usage
|
|
85
|
+
**Supported integrations:** Slack, Google Calendar, Google Drive, Gmail, Google Sheets, Google Docs, Google Slides, Notion, Salesforce, HubSpot, LinkedIn, TikTok
|
|
88
86
|
|
|
87
|
+
**Example workflow:**
|
|
89
88
|
```bash
|
|
90
|
-
#
|
|
91
|
-
base44
|
|
92
|
-
|
|
93
|
-
#
|
|
94
|
-
base44
|
|
89
|
+
# Add a connector interactively (saves to config.jsonc and opens OAuth)
|
|
90
|
+
base44 connectors add slack
|
|
91
|
+
|
|
92
|
+
# List connectors showing local vs connected status
|
|
93
|
+
base44 connectors list
|
|
94
|
+
# Output:
|
|
95
|
+
# ● Slack - user@example.com
|
|
96
|
+
# ○ Google Calendar (not connected)
|
|
97
|
+
|
|
98
|
+
# Sync all connectors (connects new, removes missing from config)
|
|
99
|
+
base44 connectors push
|
|
100
|
+
# Output:
|
|
101
|
+
# 1 connector to connect:
|
|
102
|
+
# + Google Calendar
|
|
103
|
+
# 1 connector to remove:
|
|
104
|
+
# - Notion (user@example.com)
|
|
105
|
+
# Apply 2 changes? (Y/n)
|
|
106
|
+
|
|
107
|
+
# Remove a single connector
|
|
108
|
+
base44 connectors remove slack
|
|
95
109
|
```
|
|
96
110
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
| File | Contents |
|
|
100
|
-
|------|----------|
|
|
101
|
-
| `entities.ts` | Entity interfaces, CreateInput, UpdateInput, Filter types |
|
|
102
|
-
| `client.ts` | Typed SDK client interface |
|
|
103
|
-
| `index.ts` | Barrel exports |
|
|
104
|
-
|
|
105
|
-
### Setup with @base44/sdk
|
|
106
|
-
|
|
107
|
-
1. Generate types:
|
|
108
|
-
```bash
|
|
109
|
-
base44 types
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
2. Add to `tsconfig.json`:
|
|
113
|
-
```json
|
|
114
|
-
{
|
|
115
|
-
"include": ["src", "src/base44/entities.ts"]
|
|
116
|
-
}
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
3. Use in your code:
|
|
120
|
-
```typescript
|
|
121
|
-
import { createClient } from '@base44/sdk';
|
|
122
|
-
import type { TypedBase44Client } from './base44/client';
|
|
123
|
-
|
|
124
|
-
const base44 = createClient({ appId: 'my-app' }) as TypedBase44Client;
|
|
125
|
-
|
|
126
|
-
// Fully typed!
|
|
127
|
-
const { items: tasks } = await base44.entities.Task.list();
|
|
128
|
-
await base44.entities.Task.create({ title: 'Buy milk' });
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
### Example
|
|
132
|
-
|
|
133
|
-
Given an entity schema:
|
|
111
|
+
**Configuration** (`base44/config.jsonc`):
|
|
134
112
|
```jsonc
|
|
135
|
-
// base44/entities/task.jsonc
|
|
136
113
|
{
|
|
137
|
-
"name": "
|
|
138
|
-
"
|
|
139
|
-
|
|
140
|
-
"
|
|
141
|
-
|
|
142
|
-
},
|
|
143
|
-
"required": ["title"]
|
|
114
|
+
"name": "my-app",
|
|
115
|
+
"connectors": {
|
|
116
|
+
"slack": {},
|
|
117
|
+
"googlecalendar": { "scopes": ["calendar.readonly"] }
|
|
118
|
+
}
|
|
144
119
|
}
|
|
145
120
|
```
|
|
146
121
|
|
|
147
|
-
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
title: string;
|
|
151
|
-
completed?: boolean;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export interface TaskCreateInput {
|
|
155
|
-
title: string;
|
|
156
|
-
completed?: boolean;
|
|
157
|
-
}
|
|
122
|
+
Once connected, use the SDK's `connectors.getAccessToken()` to retrieve tokens:
|
|
123
|
+
```javascript
|
|
124
|
+
const token = await base44.connectors.getAccessToken("slack");
|
|
158
125
|
```
|
|
159
126
|
|
|
160
127
|
## Configuration
|
|
@@ -202,12 +169,7 @@ my-project/
|
|
|
202
169
|
│ └── my-function/
|
|
203
170
|
│ ├── config.jsonc
|
|
204
171
|
│ └── index.js
|
|
205
|
-
├── src/
|
|
206
|
-
│ ├── base44/ # Generated types (from `base44 types`)
|
|
207
|
-
│ │ ├── entities.ts
|
|
208
|
-
│ │ ├── client.ts
|
|
209
|
-
│ │ └── index.ts
|
|
210
|
-
│ └── ... # Your frontend code
|
|
172
|
+
├── src/ # Your frontend code
|
|
211
173
|
├── dist/ # Built site files (for deployment)
|
|
212
174
|
└── package.json
|
|
213
175
|
```
|
package/dist/cli/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { EventEmitter, addAbortListener, on, once, setMaxListeners } from "node:events";
|
|
4
4
|
import childProcess, { ChildProcess, execFile, spawn, spawnSync } from "node:child_process";
|
|
5
|
-
import path, { basename, dirname, join, posix,
|
|
5
|
+
import path, { basename, dirname, join, posix, resolve, win32 } from "node:path";
|
|
6
6
|
import fs, { appendFileSync, createReadStream, createWriteStream, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
7
7
|
import y, { execArgv, execPath, hrtime, platform, stdin, stdout } from "node:process";
|
|
8
8
|
import { aborted, callbackify, debuglog, inspect, promisify, stripVTControlCharacters } from "node:util";
|
|
@@ -4547,6 +4547,7 @@ const string$1 = (params) => {
|
|
|
4547
4547
|
};
|
|
4548
4548
|
const integer = /^-?\d+$/;
|
|
4549
4549
|
const number$1 = /^-?\d+(?:\.\d+)?$/;
|
|
4550
|
+
const boolean$1 = /^(?:true|false)$/i;
|
|
4550
4551
|
const lowercase = /^[^A-Z]*$/;
|
|
4551
4552
|
const uppercase = /^[^a-z]*$/;
|
|
4552
4553
|
|
|
@@ -5325,6 +5326,24 @@ const $ZodNumberFormat = /* @__PURE__ */ $constructor("$ZodNumberFormat", (inst,
|
|
|
5325
5326
|
$ZodCheckNumberFormat.init(inst, def);
|
|
5326
5327
|
$ZodNumber.init(inst, def);
|
|
5327
5328
|
});
|
|
5329
|
+
const $ZodBoolean = /* @__PURE__ */ $constructor("$ZodBoolean", (inst, def) => {
|
|
5330
|
+
$ZodType.init(inst, def);
|
|
5331
|
+
inst._zod.pattern = boolean$1;
|
|
5332
|
+
inst._zod.parse = (payload, _ctx) => {
|
|
5333
|
+
if (def.coerce) try {
|
|
5334
|
+
payload.value = Boolean(payload.value);
|
|
5335
|
+
} catch (_$2) {}
|
|
5336
|
+
const input = payload.value;
|
|
5337
|
+
if (typeof input === "boolean") return payload;
|
|
5338
|
+
payload.issues.push({
|
|
5339
|
+
expected: "boolean",
|
|
5340
|
+
code: "invalid_type",
|
|
5341
|
+
input,
|
|
5342
|
+
inst
|
|
5343
|
+
});
|
|
5344
|
+
return payload;
|
|
5345
|
+
};
|
|
5346
|
+
});
|
|
5328
5347
|
const $ZodUnknown = /* @__PURE__ */ $constructor("$ZodUnknown", (inst, def) => {
|
|
5329
5348
|
$ZodType.init(inst, def);
|
|
5330
5349
|
inst._zod.parse = (payload) => payload;
|
|
@@ -5909,24 +5928,6 @@ const $ZodEnum = /* @__PURE__ */ $constructor("$ZodEnum", (inst, def) => {
|
|
|
5909
5928
|
return payload;
|
|
5910
5929
|
};
|
|
5911
5930
|
});
|
|
5912
|
-
const $ZodLiteral = /* @__PURE__ */ $constructor("$ZodLiteral", (inst, def) => {
|
|
5913
|
-
$ZodType.init(inst, def);
|
|
5914
|
-
if (def.values.length === 0) throw new Error("Cannot create literal schema with no valid values");
|
|
5915
|
-
const values = new Set(def.values);
|
|
5916
|
-
inst._zod.values = values;
|
|
5917
|
-
inst._zod.pattern = /* @__PURE__ */ new RegExp(`^(${def.values.map((o$2) => typeof o$2 === "string" ? escapeRegex(o$2) : o$2 ? escapeRegex(o$2.toString()) : String(o$2)).join("|")})$`);
|
|
5918
|
-
inst._zod.parse = (payload, _ctx) => {
|
|
5919
|
-
const input = payload.value;
|
|
5920
|
-
if (values.has(input)) return payload;
|
|
5921
|
-
payload.issues.push({
|
|
5922
|
-
code: "invalid_value",
|
|
5923
|
-
values: def.values,
|
|
5924
|
-
input,
|
|
5925
|
-
inst
|
|
5926
|
-
});
|
|
5927
|
-
return payload;
|
|
5928
|
-
};
|
|
5929
|
-
});
|
|
5930
5931
|
const $ZodTransform = /* @__PURE__ */ $constructor("$ZodTransform", (inst, def) => {
|
|
5931
5932
|
$ZodType.init(inst, def);
|
|
5932
5933
|
inst._zod.parse = (payload, ctx) => {
|
|
@@ -6488,6 +6489,13 @@ function _int(Class, params) {
|
|
|
6488
6489
|
});
|
|
6489
6490
|
}
|
|
6490
6491
|
/* @__NO_SIDE_EFFECTS__ */
|
|
6492
|
+
function _boolean(Class, params) {
|
|
6493
|
+
return new Class({
|
|
6494
|
+
type: "boolean",
|
|
6495
|
+
...normalizeParams(params)
|
|
6496
|
+
});
|
|
6497
|
+
}
|
|
6498
|
+
/* @__NO_SIDE_EFFECTS__ */
|
|
6491
6499
|
function _unknown(Class) {
|
|
6492
6500
|
return new Class({ type: "unknown" });
|
|
6493
6501
|
}
|
|
@@ -7058,6 +7066,9 @@ const numberProcessor = (schema, ctx, _json, _params) => {
|
|
|
7058
7066
|
}
|
|
7059
7067
|
if (typeof multipleOf === "number") json.multipleOf = multipleOf;
|
|
7060
7068
|
};
|
|
7069
|
+
const booleanProcessor = (_schema, _ctx, json, _params) => {
|
|
7070
|
+
json.type = "boolean";
|
|
7071
|
+
};
|
|
7061
7072
|
const neverProcessor = (_schema, _ctx, json, _params) => {
|
|
7062
7073
|
json.not = {};
|
|
7063
7074
|
};
|
|
@@ -7069,27 +7080,6 @@ const enumProcessor = (schema, _ctx, json, _params) => {
|
|
|
7069
7080
|
if (values.every((v$1) => typeof v$1 === "string")) json.type = "string";
|
|
7070
7081
|
json.enum = values;
|
|
7071
7082
|
};
|
|
7072
|
-
const literalProcessor = (schema, ctx, json, _params) => {
|
|
7073
|
-
const def = schema._zod.def;
|
|
7074
|
-
const vals = [];
|
|
7075
|
-
for (const val of def.values) if (val === void 0) {
|
|
7076
|
-
if (ctx.unrepresentable === "throw") throw new Error("Literal `undefined` cannot be represented in JSON Schema");
|
|
7077
|
-
} else if (typeof val === "bigint") if (ctx.unrepresentable === "throw") throw new Error("BigInt literals cannot be represented in JSON Schema");
|
|
7078
|
-
else vals.push(Number(val));
|
|
7079
|
-
else vals.push(val);
|
|
7080
|
-
if (vals.length === 0) {} else if (vals.length === 1) {
|
|
7081
|
-
const val = vals[0];
|
|
7082
|
-
json.type = val === null ? "null" : typeof val;
|
|
7083
|
-
if (ctx.target === "draft-04" || ctx.target === "openapi-3.0") json.enum = [val];
|
|
7084
|
-
else json.const = val;
|
|
7085
|
-
} else {
|
|
7086
|
-
if (vals.every((v$1) => typeof v$1 === "number")) json.type = "number";
|
|
7087
|
-
if (vals.every((v$1) => typeof v$1 === "string")) json.type = "string";
|
|
7088
|
-
if (vals.every((v$1) => typeof v$1 === "boolean")) json.type = "boolean";
|
|
7089
|
-
if (vals.every((v$1) => v$1 === null)) json.type = "null";
|
|
7090
|
-
json.enum = vals;
|
|
7091
|
-
}
|
|
7092
|
-
};
|
|
7093
7083
|
const customProcessor = (_schema, ctx, _json, _params) => {
|
|
7094
7084
|
if (ctx.unrepresentable === "throw") throw new Error("Custom types cannot be represented in JSON Schema");
|
|
7095
7085
|
};
|
|
@@ -7635,6 +7625,14 @@ const ZodNumberFormat = /* @__PURE__ */ $constructor("ZodNumberFormat", (inst, d
|
|
|
7635
7625
|
function int(params) {
|
|
7636
7626
|
return _int(ZodNumberFormat, params);
|
|
7637
7627
|
}
|
|
7628
|
+
const ZodBoolean = /* @__PURE__ */ $constructor("ZodBoolean", (inst, def) => {
|
|
7629
|
+
$ZodBoolean.init(inst, def);
|
|
7630
|
+
ZodType.init(inst, def);
|
|
7631
|
+
inst._zod.processJSONSchema = (ctx, json, params) => booleanProcessor(inst, ctx, json, params);
|
|
7632
|
+
});
|
|
7633
|
+
function boolean(params) {
|
|
7634
|
+
return _boolean(ZodBoolean, params);
|
|
7635
|
+
}
|
|
7638
7636
|
const ZodUnknown = /* @__PURE__ */ $constructor("ZodUnknown", (inst, def) => {
|
|
7639
7637
|
$ZodUnknown.init(inst, def);
|
|
7640
7638
|
ZodType.init(inst, def);
|
|
@@ -7816,23 +7814,6 @@ function _enum(values, params) {
|
|
|
7816
7814
|
...normalizeParams(params)
|
|
7817
7815
|
});
|
|
7818
7816
|
}
|
|
7819
|
-
const ZodLiteral = /* @__PURE__ */ $constructor("ZodLiteral", (inst, def) => {
|
|
7820
|
-
$ZodLiteral.init(inst, def);
|
|
7821
|
-
ZodType.init(inst, def);
|
|
7822
|
-
inst._zod.processJSONSchema = (ctx, json, params) => literalProcessor(inst, ctx, json, params);
|
|
7823
|
-
inst.values = new Set(def.values);
|
|
7824
|
-
Object.defineProperty(inst, "value", { get() {
|
|
7825
|
-
if (def.values.length > 1) throw new Error("This schema contains multiple valid literal values. Use `.values` instead.");
|
|
7826
|
-
return def.values[0];
|
|
7827
|
-
} });
|
|
7828
|
-
});
|
|
7829
|
-
function literal(value, params) {
|
|
7830
|
-
return new ZodLiteral({
|
|
7831
|
-
type: "literal",
|
|
7832
|
-
values: Array.isArray(value) ? value : [value],
|
|
7833
|
-
...normalizeParams(params)
|
|
7834
|
-
});
|
|
7835
|
-
}
|
|
7836
7817
|
const ZodTransform = /* @__PURE__ */ $constructor("ZodTransform", (inst, def) => {
|
|
7837
7818
|
$ZodTransform.init(inst, def);
|
|
7838
7819
|
ZodType.init(inst, def);
|
|
@@ -8058,6 +8039,19 @@ var AuthValidationError = class extends Error {
|
|
|
8058
8039
|
this.name = "AuthValidationError";
|
|
8059
8040
|
}
|
|
8060
8041
|
};
|
|
8042
|
+
var ConnectorApiError = class extends Error {
|
|
8043
|
+
constructor(message, cause) {
|
|
8044
|
+
super(message);
|
|
8045
|
+
this.cause = cause;
|
|
8046
|
+
this.name = "ConnectorApiError";
|
|
8047
|
+
}
|
|
8048
|
+
};
|
|
8049
|
+
var ConnectorValidationError = class extends Error {
|
|
8050
|
+
constructor(message) {
|
|
8051
|
+
super(message);
|
|
8052
|
+
this.name = "ConnectorValidationError";
|
|
8053
|
+
}
|
|
8054
|
+
};
|
|
8061
8055
|
|
|
8062
8056
|
//#endregion
|
|
8063
8057
|
//#region src/core/consts.ts
|
|
@@ -13782,10 +13776,10 @@ var require_pattern = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
13782
13776
|
exports.removeDuplicateSlashes = removeDuplicateSlashes;
|
|
13783
13777
|
function partitionAbsoluteAndRelative(patterns) {
|
|
13784
13778
|
const absolute = [];
|
|
13785
|
-
const relative
|
|
13779
|
+
const relative = [];
|
|
13786
13780
|
for (const pattern of patterns) if (isAbsolute(pattern)) absolute.push(pattern);
|
|
13787
|
-
else relative
|
|
13788
|
-
return [absolute, relative
|
|
13781
|
+
else relative.push(pattern);
|
|
13782
|
+
return [absolute, relative];
|
|
13789
13783
|
}
|
|
13790
13784
|
exports.partitionAbsoluteAndRelative = partitionAbsoluteAndRelative;
|
|
13791
13785
|
function isAbsolute(pattern) {
|
|
@@ -16855,10 +16849,13 @@ const SiteConfigSchema = object({
|
|
|
16855
16849
|
outputDirectory: string().optional(),
|
|
16856
16850
|
installCommand: string().optional()
|
|
16857
16851
|
});
|
|
16852
|
+
const ConnectorConfigSchema = object({ scopes: array(string()).optional() });
|
|
16853
|
+
const ConnectorsConfigSchema = record(string(), ConnectorConfigSchema);
|
|
16858
16854
|
const ProjectConfigSchema = object({
|
|
16859
16855
|
name: string().min(1, "App name cannot be empty"),
|
|
16860
16856
|
description: string().optional(),
|
|
16861
16857
|
site: SiteConfigSchema.optional(),
|
|
16858
|
+
connectors: ConnectorsConfigSchema.optional(),
|
|
16862
16859
|
entitiesDir: string().optional().default("entities"),
|
|
16863
16860
|
functionsDir: string().optional().default("functions")
|
|
16864
16861
|
});
|
|
@@ -28210,7 +28207,7 @@ const FILE$1 = Symbol("file");
|
|
|
28210
28207
|
const DIRECTORY$1 = Symbol("directory");
|
|
28211
28208
|
const SYMLINK$1 = Symbol("symlink");
|
|
28212
28209
|
const HARDLINK$1 = Symbol("hardlink");
|
|
28213
|
-
const HEADER
|
|
28210
|
+
const HEADER = Symbol("header");
|
|
28214
28211
|
const READ = Symbol("read");
|
|
28215
28212
|
const LSTAT = Symbol("lstat");
|
|
28216
28213
|
const ONLSTAT = Symbol("onlstat");
|
|
@@ -28329,7 +28326,7 @@ var WriteEntry = class extends Minipass {
|
|
|
28329
28326
|
[PREFIX](path$16) {
|
|
28330
28327
|
return prefixPath(path$16, this.prefix);
|
|
28331
28328
|
}
|
|
28332
|
-
[HEADER
|
|
28329
|
+
[HEADER]() {
|
|
28333
28330
|
/* c8 ignore start */
|
|
28334
28331
|
if (!this.stat) throw new Error("cannot write header before stat");
|
|
28335
28332
|
/* c8 ignore stop */
|
|
@@ -28374,7 +28371,7 @@ var WriteEntry = class extends Minipass {
|
|
|
28374
28371
|
/* c8 ignore stop */
|
|
28375
28372
|
if (this.path.slice(-1) !== "/") this.path += "/";
|
|
28376
28373
|
this.stat.size = 0;
|
|
28377
|
-
this[HEADER
|
|
28374
|
+
this[HEADER]();
|
|
28378
28375
|
this.end();
|
|
28379
28376
|
}
|
|
28380
28377
|
[SYMLINK$1]() {
|
|
@@ -28385,7 +28382,7 @@ var WriteEntry = class extends Minipass {
|
|
|
28385
28382
|
}
|
|
28386
28383
|
[ONREADLINK](linkpath) {
|
|
28387
28384
|
this.linkpath = normalizeWindowsPath(linkpath);
|
|
28388
|
-
this[HEADER
|
|
28385
|
+
this[HEADER]();
|
|
28389
28386
|
this.end();
|
|
28390
28387
|
}
|
|
28391
28388
|
[HARDLINK$1](linkpath) {
|
|
@@ -28395,7 +28392,7 @@ var WriteEntry = class extends Minipass {
|
|
|
28395
28392
|
this.type = "Link";
|
|
28396
28393
|
this.linkpath = normalizeWindowsPath(path$1.relative(this.cwd, linkpath));
|
|
28397
28394
|
this.stat.size = 0;
|
|
28398
|
-
this[HEADER
|
|
28395
|
+
this[HEADER]();
|
|
28399
28396
|
this.end();
|
|
28400
28397
|
}
|
|
28401
28398
|
[FILE$1]() {
|
|
@@ -28408,7 +28405,7 @@ var WriteEntry = class extends Minipass {
|
|
|
28408
28405
|
if (linkpath?.indexOf(this.cwd) === 0) return this[HARDLINK$1](linkpath);
|
|
28409
28406
|
this.linkCache.set(linkKey, this.absolute);
|
|
28410
28407
|
}
|
|
28411
|
-
this[HEADER
|
|
28408
|
+
this[HEADER]();
|
|
28412
28409
|
if (this.stat.size === 0) return this.end();
|
|
28413
28410
|
this[OPENFILE]();
|
|
28414
28411
|
}
|
|
@@ -31150,7 +31147,10 @@ const theme = {
|
|
|
31150
31147
|
base44OrangeBackground: source_default.bgHex("#E86B3C"),
|
|
31151
31148
|
shinyOrange: source_default.hex("#FFD700"),
|
|
31152
31149
|
links: source_default.hex("#00D4FF"),
|
|
31153
|
-
white: source_default.white
|
|
31150
|
+
white: source_default.white,
|
|
31151
|
+
success: source_default.green,
|
|
31152
|
+
warning: source_default.yellow,
|
|
31153
|
+
error: source_default.red
|
|
31154
31154
|
},
|
|
31155
31155
|
styles: {
|
|
31156
31156
|
header: source_default.dim,
|
|
@@ -31461,237 +31461,6 @@ const logoutCommand = new Command("logout").description("Logout from current dev
|
|
|
31461
31461
|
await runCommand(logout, { requireAppConfig: false });
|
|
31462
31462
|
});
|
|
31463
31463
|
|
|
31464
|
-
//#endregion
|
|
31465
|
-
//#region src/core/types/schema.ts
|
|
31466
|
-
/**
|
|
31467
|
-
* Schema for a full entity definition
|
|
31468
|
-
* Uses loose validation to allow JSON Schema flexibility
|
|
31469
|
-
*/
|
|
31470
|
-
const EntityDefinitionSchema = object({
|
|
31471
|
-
name: string().min(1, "Entity name cannot be empty"),
|
|
31472
|
-
type: literal("object").optional(),
|
|
31473
|
-
properties: record(string(), unknown()).optional(),
|
|
31474
|
-
required: array(string()).optional()
|
|
31475
|
-
}).passthrough();
|
|
31476
|
-
|
|
31477
|
-
//#endregion
|
|
31478
|
-
//#region src/core/types/generator.ts
|
|
31479
|
-
const HEADER = `// Auto-generated by Base44 CLI - DO NOT EDIT
|
|
31480
|
-
// Regenerate with: base44 types
|
|
31481
|
-
`;
|
|
31482
|
-
/**
|
|
31483
|
-
* Convert a JSON Schema type to TypeScript type
|
|
31484
|
-
*/
|
|
31485
|
-
function fieldToTypeScript(field, required$1) {
|
|
31486
|
-
let tsType;
|
|
31487
|
-
if (field.enum && field.enum.length > 0) tsType = field.enum.map((v$1) => `'${v$1}'`).join(" | ");
|
|
31488
|
-
else switch (field.type) {
|
|
31489
|
-
case "string":
|
|
31490
|
-
tsType = "string";
|
|
31491
|
-
break;
|
|
31492
|
-
case "number":
|
|
31493
|
-
tsType = "number";
|
|
31494
|
-
break;
|
|
31495
|
-
case "boolean":
|
|
31496
|
-
tsType = "boolean";
|
|
31497
|
-
break;
|
|
31498
|
-
case "array":
|
|
31499
|
-
if (field.items) tsType = `${fieldToTypeScript(field.items, true)}[]`;
|
|
31500
|
-
else tsType = "unknown[]";
|
|
31501
|
-
break;
|
|
31502
|
-
case "object":
|
|
31503
|
-
if (field.properties) tsType = `{ ${Object.entries(field.properties).map(([name$1, prop]) => {
|
|
31504
|
-
const isRequired = field.required?.includes(name$1) ?? false;
|
|
31505
|
-
const propType = fieldToTypeScript(prop, isRequired);
|
|
31506
|
-
return `${name$1}${isRequired ? "" : "?"}: ${propType}`;
|
|
31507
|
-
}).join("; ")} }`;
|
|
31508
|
-
else tsType = "Record<string, unknown>";
|
|
31509
|
-
break;
|
|
31510
|
-
default: tsType = "unknown";
|
|
31511
|
-
}
|
|
31512
|
-
return tsType;
|
|
31513
|
-
}
|
|
31514
|
-
/**
|
|
31515
|
-
* Generate interface fields for an entity
|
|
31516
|
-
*/
|
|
31517
|
-
function generateInterfaceFields(entity, options = {}) {
|
|
31518
|
-
if (!entity.properties) return "";
|
|
31519
|
-
const requiredFields = new Set(entity.required ?? []);
|
|
31520
|
-
return Object.entries(entity.properties).map(([fieldName, field]) => {
|
|
31521
|
-
const isRequired = options.allOptional ? false : requiredFields.has(fieldName);
|
|
31522
|
-
const tsType = fieldToTypeScript(field, isRequired);
|
|
31523
|
-
const optional$1 = isRequired ? "" : "?";
|
|
31524
|
-
return `${field.description ? ` /** ${field.description} */\n` : ""} ${fieldName}${optional$1}: ${tsType};`;
|
|
31525
|
-
}).join("\n");
|
|
31526
|
-
}
|
|
31527
|
-
/**
|
|
31528
|
-
* Generate TypeScript interfaces for a single entity
|
|
31529
|
-
*/
|
|
31530
|
-
function generateEntityTypes(entity) {
|
|
31531
|
-
const name$1 = entity.name;
|
|
31532
|
-
return `
|
|
31533
|
-
/** ${name$1} entity */
|
|
31534
|
-
export interface ${name$1} extends BaseEntity {
|
|
31535
|
-
${generateInterfaceFields(entity)}
|
|
31536
|
-
}
|
|
31537
|
-
|
|
31538
|
-
/** Input for creating a ${name$1} */
|
|
31539
|
-
export interface ${name$1}CreateInput {
|
|
31540
|
-
${generateInterfaceFields(entity)}
|
|
31541
|
-
}
|
|
31542
|
-
|
|
31543
|
-
/** Input for updating a ${name$1} (all fields optional) */
|
|
31544
|
-
export interface ${name$1}UpdateInput {
|
|
31545
|
-
${generateInterfaceFields(entity, { allOptional: true })}
|
|
31546
|
-
}
|
|
31547
|
-
|
|
31548
|
-
/** Filter query for ${name$1} */
|
|
31549
|
-
export interface ${name$1}Filter {
|
|
31550
|
-
${generateInterfaceFields(entity, { allOptional: true })}
|
|
31551
|
-
}
|
|
31552
|
-
`;
|
|
31553
|
-
}
|
|
31554
|
-
/**
|
|
31555
|
-
* Generate the entities.ts file content
|
|
31556
|
-
*/
|
|
31557
|
-
function generateEntitiesFile(entities) {
|
|
31558
|
-
const baseEntity = `
|
|
31559
|
-
/** System fields present on all entities */
|
|
31560
|
-
export interface BaseEntity {
|
|
31561
|
-
id: string;
|
|
31562
|
-
created_date: string;
|
|
31563
|
-
updated_date: string;
|
|
31564
|
-
}
|
|
31565
|
-
`;
|
|
31566
|
-
const entityTypes = entities.map(generateEntityTypes).join("\n");
|
|
31567
|
-
const entityNames = entities.map((e$1) => `'${e$1.name}'`).join(" | ");
|
|
31568
|
-
const entityNamesType = entities.length > 0 ? `\n/** All entity names in this project */\nexport type EntityName = ${entityNames};\n` : "";
|
|
31569
|
-
return HEADER + baseEntity + entityTypes + entityNamesType;
|
|
31570
|
-
}
|
|
31571
|
-
/**
|
|
31572
|
-
* Generate the client.ts file content with typed SDK wrapper
|
|
31573
|
-
*/
|
|
31574
|
-
function generateClientFile(entities) {
|
|
31575
|
-
if (entities.length === 0) return HEADER + "\n// No entities found\nexport {};\n";
|
|
31576
|
-
return `${HEADER}
|
|
31577
|
-
import type {
|
|
31578
|
-
${entities.map((e$1) => `${e$1.name}, ${e$1.name}CreateInput, ${e$1.name}UpdateInput, ${e$1.name}Filter`).join(",\n ")}
|
|
31579
|
-
} from './entities.js';
|
|
31580
|
-
|
|
31581
|
-
/** Response for list/filter operations */
|
|
31582
|
-
export interface ListResponse<T> {
|
|
31583
|
-
items: T[];
|
|
31584
|
-
total: number;
|
|
31585
|
-
}
|
|
31586
|
-
|
|
31587
|
-
/** Real-time subscription event */
|
|
31588
|
-
export interface RealtimeEvent<T> {
|
|
31589
|
-
type: 'create' | 'update' | 'delete';
|
|
31590
|
-
data: T;
|
|
31591
|
-
id: string;
|
|
31592
|
-
timestamp: string;
|
|
31593
|
-
}
|
|
31594
|
-
|
|
31595
|
-
/** Entity handler with CRUD operations */
|
|
31596
|
-
export interface EntityHandler<T, TCreate, TUpdate, TFilter> {
|
|
31597
|
-
/** Get all records with optional pagination */
|
|
31598
|
-
list(sort?: string, limit?: number, skip?: number, fields?: (keyof T)[]): Promise<ListResponse<T>>;
|
|
31599
|
-
/** Get records matching filter criteria */
|
|
31600
|
-
filter(query: TFilter, sort?: string, limit?: number, skip?: number, fields?: (keyof T)[]): Promise<ListResponse<T>>;
|
|
31601
|
-
/** Get a single record by ID */
|
|
31602
|
-
get(id: string): Promise<T>;
|
|
31603
|
-
/** Create a new record */
|
|
31604
|
-
create(data: TCreate): Promise<T>;
|
|
31605
|
-
/** Update an existing record */
|
|
31606
|
-
update(id: string, data: TUpdate): Promise<T>;
|
|
31607
|
-
/** Delete a record */
|
|
31608
|
-
delete(id: string): Promise<void>;
|
|
31609
|
-
/** Delete multiple records matching filter */
|
|
31610
|
-
deleteMany(query: TFilter): Promise<void>;
|
|
31611
|
-
/** Create multiple records at once */
|
|
31612
|
-
bulkCreate(data: TCreate[]): Promise<T[]>;
|
|
31613
|
-
/** Subscribe to real-time updates */
|
|
31614
|
-
subscribe(callback: (event: RealtimeEvent<T>) => void): () => void;
|
|
31615
|
-
}
|
|
31616
|
-
|
|
31617
|
-
/** Typed entities interface for Base44 SDK */
|
|
31618
|
-
export interface TypedEntities {
|
|
31619
|
-
${entities.map((e$1) => ` ${e$1.name}: EntityHandler<${e$1.name}, ${e$1.name}CreateInput, ${e$1.name}UpdateInput, ${e$1.name}Filter>;`).join("\n")}
|
|
31620
|
-
}
|
|
31621
|
-
|
|
31622
|
-
/**
|
|
31623
|
-
* Typed Base44 client interface.
|
|
31624
|
-
* Use with the Base44 SDK for type-safe entity access.
|
|
31625
|
-
*
|
|
31626
|
-
* @example
|
|
31627
|
-
* import { createClient } from '@base44/sdk';
|
|
31628
|
-
* import type { TypedBase44Client } from './base44/client';
|
|
31629
|
-
*
|
|
31630
|
-
* const base44 = createClient({ appId: 'your-app-id' }) as TypedBase44Client;
|
|
31631
|
-
* const tasks = await base44.entities.Task.list();
|
|
31632
|
-
*/
|
|
31633
|
-
export interface TypedBase44Client {
|
|
31634
|
-
entities: TypedEntities;
|
|
31635
|
-
}
|
|
31636
|
-
`;
|
|
31637
|
-
}
|
|
31638
|
-
/**
|
|
31639
|
-
* Generate the index.ts barrel file
|
|
31640
|
-
*/
|
|
31641
|
-
function generateIndexFile() {
|
|
31642
|
-
return `${HEADER}
|
|
31643
|
-
export * from './entities.js';
|
|
31644
|
-
export * from './client.js';
|
|
31645
|
-
`;
|
|
31646
|
-
}
|
|
31647
|
-
|
|
31648
|
-
//#endregion
|
|
31649
|
-
//#region src/core/types/types.ts
|
|
31650
|
-
/**
|
|
31651
|
-
* Parse entities into EntityDefinition format for type generation
|
|
31652
|
-
*/
|
|
31653
|
-
function parseEntities(entities) {
|
|
31654
|
-
return entities.map((entity) => {
|
|
31655
|
-
const result = EntityDefinitionSchema.safeParse(entity);
|
|
31656
|
-
if (result.success) return result.data;
|
|
31657
|
-
const entityName = entity.name ?? "unknown";
|
|
31658
|
-
console.warn(`Skipping invalid entity "${entityName}": ${result.error.issues.map((i$1) => `${i$1.path.join(".")}: ${i$1.message}`).join(", ")}`);
|
|
31659
|
-
return null;
|
|
31660
|
-
}).filter((e$1) => e$1 !== null);
|
|
31661
|
-
}
|
|
31662
|
-
/**
|
|
31663
|
-
* Generate TypeScript types from local entity schemas
|
|
31664
|
-
*/
|
|
31665
|
-
async function generateTypes(options = {}) {
|
|
31666
|
-
const { output = "src/base44", entitiesOnly = false } = options;
|
|
31667
|
-
const projectRoot = await findProjectRoot();
|
|
31668
|
-
if (!projectRoot) throw new Error("Project root not found. Please run this command from within a Base44 project.");
|
|
31669
|
-
const { entities } = await readProjectConfig(projectRoot.root);
|
|
31670
|
-
const parsedEntities = parseEntities(entities);
|
|
31671
|
-
const outputDir = join(projectRoot.root, output);
|
|
31672
|
-
await mkdir(outputDir, { recursive: true });
|
|
31673
|
-
const files = [];
|
|
31674
|
-
const entitiesContent = generateEntitiesFile(parsedEntities);
|
|
31675
|
-
const entitiesPath = join(outputDir, "entities.ts");
|
|
31676
|
-
await writeFile(entitiesPath, entitiesContent, "utf-8");
|
|
31677
|
-
files.push(entitiesPath);
|
|
31678
|
-
if (!entitiesOnly) {
|
|
31679
|
-
const clientContent = generateClientFile(parsedEntities);
|
|
31680
|
-
const clientPath = join(outputDir, "client.ts");
|
|
31681
|
-
await writeFile(clientPath, clientContent, "utf-8");
|
|
31682
|
-
files.push(clientPath);
|
|
31683
|
-
const indexContent = generateIndexFile();
|
|
31684
|
-
const indexPath = join(outputDir, "index.ts");
|
|
31685
|
-
await writeFile(indexPath, indexContent, "utf-8");
|
|
31686
|
-
files.push(indexPath);
|
|
31687
|
-
}
|
|
31688
|
-
return {
|
|
31689
|
-
entityCount: parsedEntities.length,
|
|
31690
|
-
files,
|
|
31691
|
-
outputDir
|
|
31692
|
-
};
|
|
31693
|
-
}
|
|
31694
|
-
|
|
31695
31464
|
//#endregion
|
|
31696
31465
|
//#region src/cli/commands/entities/push.ts
|
|
31697
31466
|
async function pushEntitiesAction() {
|
|
@@ -31737,35 +31506,6 @@ const functionsDeployCommand = new Command("functions").description("Manage proj
|
|
|
31737
31506
|
await runCommand(deployFunctionsAction, { requireAuth: true });
|
|
31738
31507
|
}));
|
|
31739
31508
|
|
|
31740
|
-
//#endregion
|
|
31741
|
-
//#region src/cli/commands/types/generate.ts
|
|
31742
|
-
async function generateTypesAction(options) {
|
|
31743
|
-
const result = await runTask("Generating TypeScript types", async () => {
|
|
31744
|
-
return await generateTypes({
|
|
31745
|
-
output: options.output,
|
|
31746
|
-
entitiesOnly: options.entitiesOnly
|
|
31747
|
-
});
|
|
31748
|
-
}, {
|
|
31749
|
-
successMessage: "Types generated successfully",
|
|
31750
|
-
errorMessage: "Failed to generate types"
|
|
31751
|
-
});
|
|
31752
|
-
if (result.entityCount === 0) {
|
|
31753
|
-
M.warn("No entities found in project");
|
|
31754
|
-
return { outroMessage: "No types generated" };
|
|
31755
|
-
}
|
|
31756
|
-
M.info(`Generated types for ${result.entityCount} entities`);
|
|
31757
|
-
M.info(`Output directory: ${result.outputDir}`);
|
|
31758
|
-
const fileList = result.files.map((f) => ` - ${relative(process.cwd(), f)}`).join("\n");
|
|
31759
|
-
M.info(`Files:\n${fileList}`);
|
|
31760
|
-
return { outroMessage: "Types generated successfully!" };
|
|
31761
|
-
}
|
|
31762
|
-
const typesGenerateCommand = new Command("types").description("Generate TypeScript types from entity schemas").option("-o, --output <dir>", "Output directory", "src/base44").option("--entities-only", "Only generate entity types, skip client types").action(async (options) => {
|
|
31763
|
-
await runCommand(() => generateTypesAction(options), {
|
|
31764
|
-
requireAuth: false,
|
|
31765
|
-
requireAppConfig: false
|
|
31766
|
-
});
|
|
31767
|
-
});
|
|
31768
|
-
|
|
31769
31509
|
//#endregion
|
|
31770
31510
|
//#region node_modules/is-plain-obj/index.js
|
|
31771
31511
|
function isPlainObject(value) {
|
|
@@ -39244,6 +38984,664 @@ const siteDeployCommand = new Command("site").description("Manage site deploymen
|
|
|
39244
38984
|
await runCommand(() => deployAction(options), { requireAuth: true });
|
|
39245
38985
|
}));
|
|
39246
38986
|
|
|
38987
|
+
//#endregion
|
|
38988
|
+
//#region src/core/connectors/schema.ts
|
|
38989
|
+
/**
|
|
38990
|
+
* Response from POST /api/apps/{app_id}/external-auth/initiate
|
|
38991
|
+
*/
|
|
38992
|
+
const InitiateResponseSchema = object({
|
|
38993
|
+
redirect_url: string().nullish(),
|
|
38994
|
+
connection_id: string().nullish(),
|
|
38995
|
+
already_authorized: boolean().nullish(),
|
|
38996
|
+
other_user_email: string().nullish(),
|
|
38997
|
+
error: string().nullish()
|
|
38998
|
+
});
|
|
38999
|
+
/**
|
|
39000
|
+
* Response from GET /api/apps/{app_id}/external-auth/status
|
|
39001
|
+
*/
|
|
39002
|
+
const StatusResponseSchema = object({
|
|
39003
|
+
status: _enum([
|
|
39004
|
+
"ACTIVE",
|
|
39005
|
+
"PENDING",
|
|
39006
|
+
"FAILED"
|
|
39007
|
+
]),
|
|
39008
|
+
account_email: string().nullish(),
|
|
39009
|
+
error: string().nullish()
|
|
39010
|
+
}).transform((data) => ({
|
|
39011
|
+
status: data.status,
|
|
39012
|
+
accountEmail: data.account_email,
|
|
39013
|
+
error: data.error
|
|
39014
|
+
}));
|
|
39015
|
+
/**
|
|
39016
|
+
* A connected integration from the list endpoint
|
|
39017
|
+
*/
|
|
39018
|
+
const ConnectorSchema = object({
|
|
39019
|
+
integration_type: string(),
|
|
39020
|
+
status: string(),
|
|
39021
|
+
connected_at: string().nullish(),
|
|
39022
|
+
account_info: object({
|
|
39023
|
+
email: string().nullish(),
|
|
39024
|
+
name: string().nullish()
|
|
39025
|
+
}).nullish()
|
|
39026
|
+
}).transform((data) => ({
|
|
39027
|
+
integrationType: data.integration_type,
|
|
39028
|
+
status: data.status,
|
|
39029
|
+
connectedAt: data.connected_at,
|
|
39030
|
+
accountInfo: data.account_info
|
|
39031
|
+
}));
|
|
39032
|
+
/**
|
|
39033
|
+
* Response from GET /api/apps/{app_id}/external-auth/list
|
|
39034
|
+
*/
|
|
39035
|
+
const ListResponseSchema = object({ integrations: array(ConnectorSchema) });
|
|
39036
|
+
/**
|
|
39037
|
+
* Generic API error response
|
|
39038
|
+
*/
|
|
39039
|
+
const ApiErrorSchema = object({
|
|
39040
|
+
error: string(),
|
|
39041
|
+
detail: string().nullish()
|
|
39042
|
+
});
|
|
39043
|
+
|
|
39044
|
+
//#endregion
|
|
39045
|
+
//#region src/core/connectors/api.ts
|
|
39046
|
+
/**
|
|
39047
|
+
* Initiates OAuth flow for a connector integration.
|
|
39048
|
+
* Returns a redirect URL to open in the browser.
|
|
39049
|
+
*/
|
|
39050
|
+
async function initiateOAuth(integrationType, scopes = null) {
|
|
39051
|
+
const response = await getAppClient().post("external-auth/initiate", {
|
|
39052
|
+
json: {
|
|
39053
|
+
integration_type: integrationType,
|
|
39054
|
+
scopes
|
|
39055
|
+
},
|
|
39056
|
+
throwHttpErrors: false
|
|
39057
|
+
});
|
|
39058
|
+
const json = await response.json();
|
|
39059
|
+
if (!response.ok) {
|
|
39060
|
+
const errorResult = ApiErrorSchema.safeParse(json);
|
|
39061
|
+
if (errorResult.success) throw new ConnectorApiError(errorResult.data.error);
|
|
39062
|
+
throw new ConnectorApiError(`Failed to initiate OAuth: ${response.status} ${response.statusText}`);
|
|
39063
|
+
}
|
|
39064
|
+
const result = InitiateResponseSchema.safeParse(json);
|
|
39065
|
+
if (!result.success) throw new ConnectorValidationError(`Invalid initiate response from server: ${result.error.message}`);
|
|
39066
|
+
return result.data;
|
|
39067
|
+
}
|
|
39068
|
+
/**
|
|
39069
|
+
* Checks the status of an OAuth connection attempt.
|
|
39070
|
+
*/
|
|
39071
|
+
async function checkOAuthStatus(integrationType, connectionId) {
|
|
39072
|
+
const response = await getAppClient().get("external-auth/status", {
|
|
39073
|
+
searchParams: {
|
|
39074
|
+
integration_type: integrationType,
|
|
39075
|
+
connection_id: connectionId
|
|
39076
|
+
},
|
|
39077
|
+
throwHttpErrors: false
|
|
39078
|
+
});
|
|
39079
|
+
const json = await response.json();
|
|
39080
|
+
if (!response.ok) {
|
|
39081
|
+
const errorResult = ApiErrorSchema.safeParse(json);
|
|
39082
|
+
if (errorResult.success) throw new ConnectorApiError(errorResult.data.error);
|
|
39083
|
+
throw new ConnectorApiError(`Failed to check OAuth status: ${response.status} ${response.statusText}`);
|
|
39084
|
+
}
|
|
39085
|
+
const result = StatusResponseSchema.safeParse(json);
|
|
39086
|
+
if (!result.success) throw new ConnectorValidationError(`Invalid status response from server: ${result.error.message}`);
|
|
39087
|
+
return result.data;
|
|
39088
|
+
}
|
|
39089
|
+
/**
|
|
39090
|
+
* Lists all connected integrations for the current app.
|
|
39091
|
+
*/
|
|
39092
|
+
async function listConnectors() {
|
|
39093
|
+
const response = await getAppClient().get("external-auth/list", { throwHttpErrors: false });
|
|
39094
|
+
const json = await response.json();
|
|
39095
|
+
if (!response.ok) {
|
|
39096
|
+
const errorResult = ApiErrorSchema.safeParse(json);
|
|
39097
|
+
if (errorResult.success) throw new ConnectorApiError(errorResult.data.error);
|
|
39098
|
+
throw new ConnectorApiError(`Failed to list connectors: ${response.status} ${response.statusText}`);
|
|
39099
|
+
}
|
|
39100
|
+
const result = ListResponseSchema.safeParse(json);
|
|
39101
|
+
if (!result.success) throw new ConnectorValidationError(`Invalid list response from server: ${result.error.message}`);
|
|
39102
|
+
return result.data.integrations;
|
|
39103
|
+
}
|
|
39104
|
+
/**
|
|
39105
|
+
* Disconnects (soft delete) a connector integration.
|
|
39106
|
+
*/
|
|
39107
|
+
async function disconnectConnector(integrationType) {
|
|
39108
|
+
const response = await getAppClient().delete(`external-auth/integrations/${integrationType}`, { throwHttpErrors: false });
|
|
39109
|
+
if (!response.ok) {
|
|
39110
|
+
const json = await response.json();
|
|
39111
|
+
const errorResult = ApiErrorSchema.safeParse(json);
|
|
39112
|
+
if (errorResult.success) throw new ConnectorApiError(errorResult.data.error);
|
|
39113
|
+
throw new ConnectorApiError(`Failed to disconnect connector: ${response.status} ${response.statusText}`);
|
|
39114
|
+
}
|
|
39115
|
+
}
|
|
39116
|
+
/**
|
|
39117
|
+
* Removes (hard delete) a connector integration.
|
|
39118
|
+
* This permanently removes the connector and cannot be undone.
|
|
39119
|
+
*/
|
|
39120
|
+
async function removeConnector(integrationType) {
|
|
39121
|
+
const response = await getAppClient().delete(`external-auth/integrations/${integrationType}/remove`, { throwHttpErrors: false });
|
|
39122
|
+
if (!response.ok) {
|
|
39123
|
+
const json = await response.json();
|
|
39124
|
+
const errorResult = ApiErrorSchema.safeParse(json);
|
|
39125
|
+
if (errorResult.success) throw new ConnectorApiError(errorResult.data.error);
|
|
39126
|
+
throw new ConnectorApiError(`Failed to remove connector: ${response.status} ${response.statusText}`);
|
|
39127
|
+
}
|
|
39128
|
+
}
|
|
39129
|
+
|
|
39130
|
+
//#endregion
|
|
39131
|
+
//#region src/core/connectors/constants.ts
|
|
39132
|
+
/**
|
|
39133
|
+
* OAuth polling configuration
|
|
39134
|
+
*/
|
|
39135
|
+
const OAUTH_POLL_INTERVAL_MS = 2e3;
|
|
39136
|
+
const OAUTH_POLL_TIMEOUT_MS = 300 * 1e3;
|
|
39137
|
+
/**
|
|
39138
|
+
* Supported OAuth connector integrations.
|
|
39139
|
+
* Based on apper/backend/app/external_auth/models/constants.py
|
|
39140
|
+
*/
|
|
39141
|
+
const SUPPORTED_INTEGRATIONS = [
|
|
39142
|
+
"googlecalendar",
|
|
39143
|
+
"googledrive",
|
|
39144
|
+
"gmail",
|
|
39145
|
+
"googlesheets",
|
|
39146
|
+
"googledocs",
|
|
39147
|
+
"googleslides",
|
|
39148
|
+
"slack",
|
|
39149
|
+
"notion",
|
|
39150
|
+
"salesforce",
|
|
39151
|
+
"hubspot",
|
|
39152
|
+
"linkedin",
|
|
39153
|
+
"tiktok"
|
|
39154
|
+
];
|
|
39155
|
+
/**
|
|
39156
|
+
* Display names for integrations (for CLI output)
|
|
39157
|
+
*/
|
|
39158
|
+
const INTEGRATION_DISPLAY_NAMES = {
|
|
39159
|
+
googlecalendar: "Google Calendar",
|
|
39160
|
+
googledrive: "Google Drive",
|
|
39161
|
+
gmail: "Gmail",
|
|
39162
|
+
googlesheets: "Google Sheets",
|
|
39163
|
+
googledocs: "Google Docs",
|
|
39164
|
+
googleslides: "Google Slides",
|
|
39165
|
+
slack: "Slack",
|
|
39166
|
+
notion: "Notion",
|
|
39167
|
+
salesforce: "Salesforce",
|
|
39168
|
+
hubspot: "HubSpot",
|
|
39169
|
+
linkedin: "LinkedIn",
|
|
39170
|
+
tiktok: "TikTok"
|
|
39171
|
+
};
|
|
39172
|
+
function isValidIntegration(type) {
|
|
39173
|
+
return SUPPORTED_INTEGRATIONS.includes(type);
|
|
39174
|
+
}
|
|
39175
|
+
function getIntegrationDisplayName(type) {
|
|
39176
|
+
if (isValidIntegration(type)) return INTEGRATION_DISPLAY_NAMES[type];
|
|
39177
|
+
return type;
|
|
39178
|
+
}
|
|
39179
|
+
|
|
39180
|
+
//#endregion
|
|
39181
|
+
//#region src/core/connectors/config.ts
|
|
39182
|
+
/**
|
|
39183
|
+
* Read all connectors from the project config file
|
|
39184
|
+
*/
|
|
39185
|
+
async function readLocalConnectors(projectRoot) {
|
|
39186
|
+
const found = await findProjectRoot(projectRoot);
|
|
39187
|
+
if (!found) return [];
|
|
39188
|
+
const connectorsData = (await readJsonFile(found.configPath)).connectors;
|
|
39189
|
+
if (!connectorsData) return [];
|
|
39190
|
+
const connectors = [];
|
|
39191
|
+
for (const [type, config$1] of Object.entries(connectorsData)) {
|
|
39192
|
+
if (!isValidIntegration(type)) throw new Error(`Unknown connector type: ${type}`);
|
|
39193
|
+
connectors.push({
|
|
39194
|
+
type,
|
|
39195
|
+
scopes: config$1.scopes
|
|
39196
|
+
});
|
|
39197
|
+
}
|
|
39198
|
+
return connectors;
|
|
39199
|
+
}
|
|
39200
|
+
/**
|
|
39201
|
+
* Write connectors to the project config file
|
|
39202
|
+
*/
|
|
39203
|
+
async function writeLocalConnectors(connectors, projectRoot) {
|
|
39204
|
+
const found = await findProjectRoot(projectRoot);
|
|
39205
|
+
if (!found) throw new Error("Project config not found. Run this command from a Base44 project directory.");
|
|
39206
|
+
const existingConfig = await readJsonFile(found.configPath);
|
|
39207
|
+
const connectorsData = {};
|
|
39208
|
+
for (const connector of connectors) connectorsData[connector.type] = { ...connector.scopes && { scopes: connector.scopes } };
|
|
39209
|
+
const updatedConfig = {
|
|
39210
|
+
...existingConfig,
|
|
39211
|
+
connectors: Object.keys(connectorsData).length > 0 ? connectorsData : void 0
|
|
39212
|
+
};
|
|
39213
|
+
if (!updatedConfig.connectors) delete updatedConfig.connectors;
|
|
39214
|
+
await writeJsonFile(found.configPath, updatedConfig);
|
|
39215
|
+
return found.configPath;
|
|
39216
|
+
}
|
|
39217
|
+
/**
|
|
39218
|
+
* Add a connector to the project config file
|
|
39219
|
+
*/
|
|
39220
|
+
async function addLocalConnector(type, scopes, projectRoot) {
|
|
39221
|
+
const connectors = await readLocalConnectors(projectRoot);
|
|
39222
|
+
const existing = connectors.find((c$1) => c$1.type === type);
|
|
39223
|
+
if (existing) {
|
|
39224
|
+
if (scopes) existing.scopes = scopes;
|
|
39225
|
+
} else connectors.push({
|
|
39226
|
+
type,
|
|
39227
|
+
scopes
|
|
39228
|
+
});
|
|
39229
|
+
return await writeLocalConnectors(connectors, projectRoot);
|
|
39230
|
+
}
|
|
39231
|
+
/**
|
|
39232
|
+
* Remove a connector from the project config file
|
|
39233
|
+
*/
|
|
39234
|
+
async function removeLocalConnector(type, projectRoot) {
|
|
39235
|
+
const connectors = await readLocalConnectors(projectRoot);
|
|
39236
|
+
const filtered = connectors.filter((c$1) => c$1.type !== type);
|
|
39237
|
+
if (filtered.length === connectors.length) return null;
|
|
39238
|
+
return await writeLocalConnectors(filtered, projectRoot);
|
|
39239
|
+
}
|
|
39240
|
+
|
|
39241
|
+
//#endregion
|
|
39242
|
+
//#region src/cli/commands/connectors/add.ts
|
|
39243
|
+
async function promptForIntegrationType() {
|
|
39244
|
+
const selected = await ve({
|
|
39245
|
+
message: "Select an integration to connect:",
|
|
39246
|
+
options: SUPPORTED_INTEGRATIONS.map((type) => ({
|
|
39247
|
+
value: type,
|
|
39248
|
+
label: getIntegrationDisplayName(type)
|
|
39249
|
+
}))
|
|
39250
|
+
});
|
|
39251
|
+
if (pD(selected)) return null;
|
|
39252
|
+
return selected;
|
|
39253
|
+
}
|
|
39254
|
+
async function waitForOAuthCompletion(integrationType, connectionId) {
|
|
39255
|
+
let accountEmail;
|
|
39256
|
+
let error;
|
|
39257
|
+
try {
|
|
39258
|
+
await runTask("Waiting for authorization...", async (updateMessage) => {
|
|
39259
|
+
await pWaitFor(async () => {
|
|
39260
|
+
const status = await checkOAuthStatus(integrationType, connectionId);
|
|
39261
|
+
if (status.status === "ACTIVE") {
|
|
39262
|
+
accountEmail = status.accountEmail ?? void 0;
|
|
39263
|
+
return true;
|
|
39264
|
+
}
|
|
39265
|
+
if (status.status === "FAILED") {
|
|
39266
|
+
error = status.error || "Authorization failed";
|
|
39267
|
+
throw new Error(error);
|
|
39268
|
+
}
|
|
39269
|
+
updateMessage("Waiting for authorization in browser...");
|
|
39270
|
+
return false;
|
|
39271
|
+
}, {
|
|
39272
|
+
interval: OAUTH_POLL_INTERVAL_MS,
|
|
39273
|
+
timeout: OAUTH_POLL_TIMEOUT_MS
|
|
39274
|
+
});
|
|
39275
|
+
}, {
|
|
39276
|
+
successMessage: "Authorization completed!",
|
|
39277
|
+
errorMessage: "Authorization failed"
|
|
39278
|
+
});
|
|
39279
|
+
return {
|
|
39280
|
+
success: true,
|
|
39281
|
+
accountEmail
|
|
39282
|
+
};
|
|
39283
|
+
} catch (err) {
|
|
39284
|
+
if (err instanceof Error && err.message.includes("timed out")) return {
|
|
39285
|
+
success: false,
|
|
39286
|
+
error: "Authorization timed out. Please try again."
|
|
39287
|
+
};
|
|
39288
|
+
return {
|
|
39289
|
+
success: false,
|
|
39290
|
+
error: error || (err instanceof Error ? err.message : "Unknown error")
|
|
39291
|
+
};
|
|
39292
|
+
}
|
|
39293
|
+
}
|
|
39294
|
+
async function addConnector(integrationType) {
|
|
39295
|
+
let selectedType;
|
|
39296
|
+
if (!integrationType) {
|
|
39297
|
+
const prompted = await promptForIntegrationType();
|
|
39298
|
+
if (!prompted) return { outroMessage: "Cancelled" };
|
|
39299
|
+
selectedType = prompted;
|
|
39300
|
+
} else {
|
|
39301
|
+
if (!isValidIntegration(integrationType)) {
|
|
39302
|
+
const supportedList = SUPPORTED_INTEGRATIONS.join(", ");
|
|
39303
|
+
throw new Error(`Unsupported connector: ${integrationType}\nSupported connectors: ${supportedList}`);
|
|
39304
|
+
}
|
|
39305
|
+
selectedType = integrationType;
|
|
39306
|
+
}
|
|
39307
|
+
const displayName = getIntegrationDisplayName(selectedType);
|
|
39308
|
+
const initiateResponse = await runTask(`Initiating ${displayName} connection...`, async () => {
|
|
39309
|
+
return await initiateOAuth(selectedType);
|
|
39310
|
+
}, {
|
|
39311
|
+
successMessage: `${displayName} OAuth initiated`,
|
|
39312
|
+
errorMessage: `Failed to initiate ${displayName} connection`
|
|
39313
|
+
});
|
|
39314
|
+
if (initiateResponse.already_authorized) {
|
|
39315
|
+
await addLocalConnector(selectedType);
|
|
39316
|
+
return { outroMessage: `Already connected to ${theme.styles.bold(displayName)} (added to config)` };
|
|
39317
|
+
}
|
|
39318
|
+
if (initiateResponse.error === "different_user" && initiateResponse.other_user_email) throw new Error(`This app is already connected to ${displayName} by ${initiateResponse.other_user_email}`);
|
|
39319
|
+
if (!initiateResponse.redirect_url || !initiateResponse.connection_id) throw new Error("Invalid response from server: missing redirect URL or connection ID");
|
|
39320
|
+
M.info(`Please authorize ${displayName} at:\n${theme.colors.links(initiateResponse.redirect_url)}`);
|
|
39321
|
+
const result = await waitForOAuthCompletion(selectedType, initiateResponse.connection_id);
|
|
39322
|
+
if (!result.success) throw new Error(result.error || "Authorization failed");
|
|
39323
|
+
await addLocalConnector(selectedType);
|
|
39324
|
+
const accountInfo = result.accountEmail ? ` as ${theme.styles.bold(result.accountEmail)}` : "";
|
|
39325
|
+
return { outroMessage: `Successfully connected to ${theme.styles.bold(displayName)}${accountInfo}` };
|
|
39326
|
+
}
|
|
39327
|
+
const connectorsAddCommand = new Command("add").argument("[type]", "Integration type (e.g., slack, notion, googlecalendar)").description("Connect an OAuth integration").action(async (type) => {
|
|
39328
|
+
await runCommand(() => addConnector(type), {
|
|
39329
|
+
requireAuth: true,
|
|
39330
|
+
requireAppConfig: true
|
|
39331
|
+
});
|
|
39332
|
+
});
|
|
39333
|
+
|
|
39334
|
+
//#endregion
|
|
39335
|
+
//#region src/cli/commands/connectors/list.ts
|
|
39336
|
+
function mergeConnectors(local, backend) {
|
|
39337
|
+
const merged = /* @__PURE__ */ new Map();
|
|
39338
|
+
for (const connector of local) merged.set(connector.type, {
|
|
39339
|
+
type: connector.type,
|
|
39340
|
+
displayName: getIntegrationDisplayName(connector.type),
|
|
39341
|
+
inLocal: true,
|
|
39342
|
+
inBackend: false
|
|
39343
|
+
});
|
|
39344
|
+
for (const connector of backend) {
|
|
39345
|
+
const existing = merged.get(connector.integrationType);
|
|
39346
|
+
const accountEmail = (connector.accountInfo?.email || connector.accountInfo?.name) ?? void 0;
|
|
39347
|
+
if (existing) {
|
|
39348
|
+
existing.inBackend = true;
|
|
39349
|
+
existing.status = connector.status;
|
|
39350
|
+
existing.accountEmail = accountEmail;
|
|
39351
|
+
} else merged.set(connector.integrationType, {
|
|
39352
|
+
type: connector.integrationType,
|
|
39353
|
+
displayName: getIntegrationDisplayName(connector.integrationType),
|
|
39354
|
+
inLocal: false,
|
|
39355
|
+
inBackend: true,
|
|
39356
|
+
status: connector.status,
|
|
39357
|
+
accountEmail
|
|
39358
|
+
});
|
|
39359
|
+
}
|
|
39360
|
+
return Array.from(merged.values());
|
|
39361
|
+
}
|
|
39362
|
+
function formatConnectorLine(connector) {
|
|
39363
|
+
const { displayName, inLocal, inBackend, status, accountEmail } = connector;
|
|
39364
|
+
const isConnected$1 = inBackend && status?.toLowerCase() === "active";
|
|
39365
|
+
const isPending = inLocal && !inBackend;
|
|
39366
|
+
const isOrphaned = inBackend && !inLocal;
|
|
39367
|
+
let bullet;
|
|
39368
|
+
let statusText = "";
|
|
39369
|
+
if (isConnected$1) {
|
|
39370
|
+
bullet = theme.colors.success("●");
|
|
39371
|
+
if (accountEmail) statusText = ` - ${accountEmail}`;
|
|
39372
|
+
} else if (isPending) {
|
|
39373
|
+
bullet = theme.colors.warning("○");
|
|
39374
|
+
statusText = theme.styles.dim(" (not connected)");
|
|
39375
|
+
} else if (isOrphaned) {
|
|
39376
|
+
bullet = theme.colors.error("○");
|
|
39377
|
+
statusText = theme.styles.dim(" (not in local config)");
|
|
39378
|
+
} else {
|
|
39379
|
+
bullet = theme.colors.error("○");
|
|
39380
|
+
statusText = theme.styles.dim(` (${status || "disconnected"})`);
|
|
39381
|
+
}
|
|
39382
|
+
return `${bullet} ${displayName}${statusText}`;
|
|
39383
|
+
}
|
|
39384
|
+
async function listConnectorsCommand() {
|
|
39385
|
+
const [localConnectors, backendConnectors] = await runTask("Fetching connectors...", async () => {
|
|
39386
|
+
const [local, backend] = await Promise.all([readLocalConnectors().catch(() => []), listConnectors().catch(() => [])]);
|
|
39387
|
+
return [local, backend];
|
|
39388
|
+
}, {
|
|
39389
|
+
successMessage: "Connectors loaded",
|
|
39390
|
+
errorMessage: "Failed to fetch connectors"
|
|
39391
|
+
});
|
|
39392
|
+
const merged = mergeConnectors(localConnectors, backendConnectors);
|
|
39393
|
+
if (merged.length === 0) {
|
|
39394
|
+
M.info("No connectors configured for this app.");
|
|
39395
|
+
M.info(`Run ${theme.styles.bold("base44 connectors add")} to connect an integration.`);
|
|
39396
|
+
return { outroMessage: "" };
|
|
39397
|
+
}
|
|
39398
|
+
console.log();
|
|
39399
|
+
for (const connector of merged) console.log(formatConnectorLine(connector));
|
|
39400
|
+
console.log();
|
|
39401
|
+
const connected = merged.filter((c$1) => c$1.inBackend && c$1.status?.toLowerCase() === "active").length;
|
|
39402
|
+
const pending = merged.filter((c$1) => c$1.inLocal && !c$1.inBackend).length;
|
|
39403
|
+
let summary = `${connected} connected`;
|
|
39404
|
+
if (pending > 0) {
|
|
39405
|
+
summary += `, ${pending} pending`;
|
|
39406
|
+
M.info(`Run ${theme.styles.bold("base44 connectors push")} to connect pending integrations.`);
|
|
39407
|
+
}
|
|
39408
|
+
return { outroMessage: summary };
|
|
39409
|
+
}
|
|
39410
|
+
const connectorsListCommand = new Command("list").description("List all connected OAuth integrations").action(async () => {
|
|
39411
|
+
await runCommand(listConnectorsCommand, {
|
|
39412
|
+
requireAuth: true,
|
|
39413
|
+
requireAppConfig: true
|
|
39414
|
+
});
|
|
39415
|
+
});
|
|
39416
|
+
|
|
39417
|
+
//#endregion
|
|
39418
|
+
//#region src/cli/commands/connectors/push.ts
|
|
39419
|
+
function findPendingConnectors(local, backend) {
|
|
39420
|
+
const connectedTypes = new Set(backend.filter((c$1) => c$1.status.toLowerCase() === "active").map((c$1) => c$1.integrationType));
|
|
39421
|
+
return local.filter((c$1) => !connectedTypes.has(c$1.type)).map((c$1) => ({
|
|
39422
|
+
type: c$1.type,
|
|
39423
|
+
displayName: getIntegrationDisplayName(c$1.type),
|
|
39424
|
+
scopes: c$1.scopes
|
|
39425
|
+
}));
|
|
39426
|
+
}
|
|
39427
|
+
function findOrphanedConnectors(local, backend) {
|
|
39428
|
+
const localTypes = new Set(local.map((c$1) => c$1.type));
|
|
39429
|
+
return backend.filter((c$1) => c$1.status.toLowerCase() === "active").filter((c$1) => !localTypes.has(c$1.integrationType)).filter((c$1) => isValidIntegration(c$1.integrationType)).map((c$1) => ({
|
|
39430
|
+
type: c$1.integrationType,
|
|
39431
|
+
displayName: getIntegrationDisplayName(c$1.integrationType),
|
|
39432
|
+
accountEmail: (c$1.accountInfo?.email || c$1.accountInfo?.name) ?? void 0
|
|
39433
|
+
}));
|
|
39434
|
+
}
|
|
39435
|
+
async function connectSingleConnector(connector) {
|
|
39436
|
+
const { type, displayName, scopes } = connector;
|
|
39437
|
+
const initiateResponse = await initiateOAuth(type, scopes || null);
|
|
39438
|
+
if (initiateResponse.already_authorized) return { success: true };
|
|
39439
|
+
if (initiateResponse.error === "different_user") return {
|
|
39440
|
+
success: false,
|
|
39441
|
+
error: `Already connected by ${initiateResponse.other_user_email}`
|
|
39442
|
+
};
|
|
39443
|
+
if (!initiateResponse.redirect_url || !initiateResponse.connection_id) return {
|
|
39444
|
+
success: false,
|
|
39445
|
+
error: "Invalid response from server"
|
|
39446
|
+
};
|
|
39447
|
+
M.info(`Please authorize ${displayName} at:\n${theme.colors.links(initiateResponse.redirect_url)}`);
|
|
39448
|
+
let accountEmail;
|
|
39449
|
+
try {
|
|
39450
|
+
await runTask("Waiting for authorization...", async () => {
|
|
39451
|
+
await pWaitFor(async () => {
|
|
39452
|
+
const status = await checkOAuthStatus(type, initiateResponse.connection_id);
|
|
39453
|
+
if (status.status === "ACTIVE") {
|
|
39454
|
+
accountEmail = status.accountEmail ?? void 0;
|
|
39455
|
+
return true;
|
|
39456
|
+
}
|
|
39457
|
+
if (status.status === "FAILED") throw new Error(status.error || "Authorization failed");
|
|
39458
|
+
return false;
|
|
39459
|
+
}, {
|
|
39460
|
+
interval: OAUTH_POLL_INTERVAL_MS,
|
|
39461
|
+
timeout: OAUTH_POLL_TIMEOUT_MS
|
|
39462
|
+
});
|
|
39463
|
+
}, {
|
|
39464
|
+
successMessage: "Authorization completed!",
|
|
39465
|
+
errorMessage: "Authorization failed"
|
|
39466
|
+
});
|
|
39467
|
+
return {
|
|
39468
|
+
success: true,
|
|
39469
|
+
accountEmail
|
|
39470
|
+
};
|
|
39471
|
+
} catch (err) {
|
|
39472
|
+
if (err instanceof Error && err.message.includes("timed out")) return {
|
|
39473
|
+
success: false,
|
|
39474
|
+
error: "Authorization timed out. Please try again."
|
|
39475
|
+
};
|
|
39476
|
+
return {
|
|
39477
|
+
success: false,
|
|
39478
|
+
error: err instanceof Error ? err.message : "Unknown error"
|
|
39479
|
+
};
|
|
39480
|
+
}
|
|
39481
|
+
}
|
|
39482
|
+
async function pushConnectorsCommand() {
|
|
39483
|
+
const [localConnectors, backendConnectors] = await runTask("Checking connector status...", async () => {
|
|
39484
|
+
const [local, backend] = await Promise.all([readLocalConnectors(), listConnectors().catch(() => [])]);
|
|
39485
|
+
return [local, backend];
|
|
39486
|
+
}, {
|
|
39487
|
+
successMessage: "Status checked",
|
|
39488
|
+
errorMessage: "Failed to check status"
|
|
39489
|
+
});
|
|
39490
|
+
const pending = findPendingConnectors(localConnectors, backendConnectors);
|
|
39491
|
+
const orphaned = findOrphanedConnectors(localConnectors, backendConnectors);
|
|
39492
|
+
if (pending.length === 0 && orphaned.length === 0) return { outroMessage: "All connectors are in sync" };
|
|
39493
|
+
console.log();
|
|
39494
|
+
if (pending.length > 0) {
|
|
39495
|
+
M.info(`${pending.length} connector${pending.length === 1 ? "" : "s"} to connect:`);
|
|
39496
|
+
for (const c$1 of pending) console.log(` ${theme.colors.success("+")} ${c$1.displayName}`);
|
|
39497
|
+
}
|
|
39498
|
+
if (orphaned.length > 0) {
|
|
39499
|
+
M.info(`${orphaned.length} connector${orphaned.length === 1 ? "" : "s"} to remove:`);
|
|
39500
|
+
for (const c$1 of orphaned) {
|
|
39501
|
+
const accountInfo = c$1.accountEmail ? ` (${c$1.accountEmail})` : "";
|
|
39502
|
+
console.log(` ${theme.colors.error("-")} ${c$1.displayName}${accountInfo}`);
|
|
39503
|
+
}
|
|
39504
|
+
}
|
|
39505
|
+
console.log();
|
|
39506
|
+
const totalChanges = pending.length + orphaned.length;
|
|
39507
|
+
const shouldProceed = await ye({
|
|
39508
|
+
message: `Apply ${totalChanges} change${totalChanges === 1 ? "" : "s"}?`,
|
|
39509
|
+
initialValue: true
|
|
39510
|
+
});
|
|
39511
|
+
if (pD(shouldProceed) || !shouldProceed) return { outroMessage: "Cancelled" };
|
|
39512
|
+
let connected = 0;
|
|
39513
|
+
let removed = 0;
|
|
39514
|
+
let failed = 0;
|
|
39515
|
+
for (const connector of orphaned) try {
|
|
39516
|
+
await disconnectConnector(connector.type);
|
|
39517
|
+
M.success(`Removed ${connector.displayName}`);
|
|
39518
|
+
removed++;
|
|
39519
|
+
} catch (err) {
|
|
39520
|
+
M.error(`Failed to remove ${connector.displayName}: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
39521
|
+
failed++;
|
|
39522
|
+
}
|
|
39523
|
+
for (const connector of pending) {
|
|
39524
|
+
console.log();
|
|
39525
|
+
M.info(`Connecting ${theme.styles.bold(connector.displayName)}...`);
|
|
39526
|
+
const result = await connectSingleConnector(connector);
|
|
39527
|
+
if (result.success) {
|
|
39528
|
+
const accountInfo = result.accountEmail ? ` as ${result.accountEmail}` : "";
|
|
39529
|
+
M.success(`${connector.displayName} connected${accountInfo}`);
|
|
39530
|
+
connected++;
|
|
39531
|
+
} else {
|
|
39532
|
+
M.error(`${connector.displayName} failed: ${result.error}`);
|
|
39533
|
+
failed++;
|
|
39534
|
+
}
|
|
39535
|
+
}
|
|
39536
|
+
console.log();
|
|
39537
|
+
const parts = [];
|
|
39538
|
+
if (connected > 0) parts.push(`${connected} connected`);
|
|
39539
|
+
if (removed > 0) parts.push(`${removed} removed`);
|
|
39540
|
+
if (failed > 0) parts.push(`${failed} failed`);
|
|
39541
|
+
return { outroMessage: parts.join(", ") };
|
|
39542
|
+
}
|
|
39543
|
+
const connectorsPushCommand = new Command("push").description("Sync connectors with backend (connect new, remove missing)").action(async () => {
|
|
39544
|
+
await runCommand(pushConnectorsCommand, {
|
|
39545
|
+
requireAuth: true,
|
|
39546
|
+
requireAppConfig: true
|
|
39547
|
+
});
|
|
39548
|
+
});
|
|
39549
|
+
|
|
39550
|
+
//#endregion
|
|
39551
|
+
//#region src/cli/commands/connectors/remove.ts
|
|
39552
|
+
function mergeConnectorsForRemoval(local, backend) {
|
|
39553
|
+
const merged = /* @__PURE__ */ new Map();
|
|
39554
|
+
for (const connector of local) merged.set(connector.type, {
|
|
39555
|
+
type: connector.type,
|
|
39556
|
+
displayName: getIntegrationDisplayName(connector.type),
|
|
39557
|
+
inLocal: true,
|
|
39558
|
+
inBackend: false
|
|
39559
|
+
});
|
|
39560
|
+
for (const connector of backend) {
|
|
39561
|
+
if (!isValidIntegration(connector.integrationType)) continue;
|
|
39562
|
+
const existing = merged.get(connector.integrationType);
|
|
39563
|
+
const accountEmail = (connector.accountInfo?.email || connector.accountInfo?.name) ?? void 0;
|
|
39564
|
+
if (existing) {
|
|
39565
|
+
existing.inBackend = true;
|
|
39566
|
+
existing.accountEmail = accountEmail;
|
|
39567
|
+
} else merged.set(connector.integrationType, {
|
|
39568
|
+
type: connector.integrationType,
|
|
39569
|
+
displayName: getIntegrationDisplayName(connector.integrationType),
|
|
39570
|
+
inLocal: false,
|
|
39571
|
+
inBackend: true,
|
|
39572
|
+
accountEmail
|
|
39573
|
+
});
|
|
39574
|
+
}
|
|
39575
|
+
return Array.from(merged.values());
|
|
39576
|
+
}
|
|
39577
|
+
async function promptForConnectorToRemove(connectors) {
|
|
39578
|
+
const selected = await ve({
|
|
39579
|
+
message: "Select a connector to remove:",
|
|
39580
|
+
options: connectors.map((c$1) => {
|
|
39581
|
+
let label = c$1.displayName;
|
|
39582
|
+
if (c$1.accountEmail) label += ` (${c$1.accountEmail})`;
|
|
39583
|
+
else if (c$1.inLocal && !c$1.inBackend) label += " (not connected)";
|
|
39584
|
+
return {
|
|
39585
|
+
value: c$1.type,
|
|
39586
|
+
label
|
|
39587
|
+
};
|
|
39588
|
+
})
|
|
39589
|
+
});
|
|
39590
|
+
if (pD(selected)) return null;
|
|
39591
|
+
return selected;
|
|
39592
|
+
}
|
|
39593
|
+
async function removeConnectorCommand(integrationType, options = {}) {
|
|
39594
|
+
const isHardDelete = options.hard === true;
|
|
39595
|
+
const [localConnectors, backendConnectors] = await runTask("Fetching connectors...", async () => {
|
|
39596
|
+
const [local, backend] = await Promise.all([readLocalConnectors().catch(() => []), listConnectors().catch(() => [])]);
|
|
39597
|
+
return [local, backend];
|
|
39598
|
+
}, {
|
|
39599
|
+
successMessage: "Connectors loaded",
|
|
39600
|
+
errorMessage: "Failed to fetch connectors"
|
|
39601
|
+
});
|
|
39602
|
+
const merged = mergeConnectorsForRemoval(localConnectors, backendConnectors);
|
|
39603
|
+
if (merged.length === 0) return { outroMessage: "No connectors to remove" };
|
|
39604
|
+
let selectedType;
|
|
39605
|
+
let selectedConnector;
|
|
39606
|
+
if (!integrationType) {
|
|
39607
|
+
const prompted = await promptForConnectorToRemove(merged);
|
|
39608
|
+
if (!prompted) return { outroMessage: "Cancelled" };
|
|
39609
|
+
selectedType = prompted;
|
|
39610
|
+
selectedConnector = merged.find((c$1) => c$1.type === selectedType);
|
|
39611
|
+
} else {
|
|
39612
|
+
if (!isValidIntegration(integrationType)) throw new Error(`Invalid connector type: ${integrationType}`);
|
|
39613
|
+
selectedConnector = merged.find((c$1) => c$1.type === integrationType);
|
|
39614
|
+
if (!selectedConnector) throw new Error(`No ${getIntegrationDisplayName(integrationType)} connector found`);
|
|
39615
|
+
selectedType = integrationType;
|
|
39616
|
+
}
|
|
39617
|
+
const displayName = getIntegrationDisplayName(selectedType);
|
|
39618
|
+
const accountInfo = selectedConnector?.accountEmail ? ` (${selectedConnector.accountEmail})` : "";
|
|
39619
|
+
const shouldRemove = await ye({
|
|
39620
|
+
message: `${isHardDelete ? "Permanently remove" : "Remove"} ${displayName}${accountInfo}?`,
|
|
39621
|
+
initialValue: false
|
|
39622
|
+
});
|
|
39623
|
+
if (pD(shouldRemove) || !shouldRemove) return { outroMessage: "Cancelled" };
|
|
39624
|
+
await runTask(isHardDelete ? `Removing ${displayName}...` : `Removing ${displayName}...`, async () => {
|
|
39625
|
+
if (selectedConnector?.inBackend) if (isHardDelete) await removeConnector(selectedType);
|
|
39626
|
+
else await disconnectConnector(selectedType);
|
|
39627
|
+
await removeLocalConnector(selectedType);
|
|
39628
|
+
}, {
|
|
39629
|
+
successMessage: `${displayName} removed`,
|
|
39630
|
+
errorMessage: `Failed to remove ${displayName}`
|
|
39631
|
+
});
|
|
39632
|
+
return { outroMessage: `Successfully removed ${theme.styles.bold(displayName)}` };
|
|
39633
|
+
}
|
|
39634
|
+
const connectorsRemoveCommand = new Command("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) => {
|
|
39635
|
+
await runCommand(() => removeConnectorCommand(type, options), {
|
|
39636
|
+
requireAuth: true,
|
|
39637
|
+
requireAppConfig: true
|
|
39638
|
+
});
|
|
39639
|
+
});
|
|
39640
|
+
|
|
39641
|
+
//#endregion
|
|
39642
|
+
//#region src/cli/commands/connectors/index.ts
|
|
39643
|
+
const connectorsCommand = new Command("connectors").description("Manage OAuth connectors").addCommand(connectorsAddCommand).addCommand(connectorsListCommand).addCommand(connectorsPushCommand).addCommand(connectorsRemoveCommand);
|
|
39644
|
+
|
|
39247
39645
|
//#endregion
|
|
39248
39646
|
//#region package.json
|
|
39249
39647
|
var version = "0.0.15";
|
|
@@ -39262,8 +39660,8 @@ program.addCommand(deployCommand);
|
|
|
39262
39660
|
program.addCommand(linkCommand);
|
|
39263
39661
|
program.addCommand(entitiesPushCommand);
|
|
39264
39662
|
program.addCommand(functionsDeployCommand);
|
|
39265
|
-
program.addCommand(typesGenerateCommand);
|
|
39266
39663
|
program.addCommand(siteDeployCommand);
|
|
39664
|
+
program.addCommand(connectorsCommand);
|
|
39267
39665
|
program.parse();
|
|
39268
39666
|
|
|
39269
39667
|
//#endregion
|
package/package.json
CHANGED