alinea 2.0.0-preview.10 → 2.0.0-preview.11
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/dist/.init/content/pages/welcome.json +1 -1
- package/dist/.mcp/later/dev.json +1 -1
- package/dist/.mcp/online/dev.json +1 -1
- package/dist/adapter/next/handler.js +6 -4
- package/dist/backend/Handler.d.ts +3 -0
- package/dist/backend/Handler.js +5 -5
- package/dist/backend/api/OAuth2.js +21 -26
- package/dist/backend/util/JWKS.d.ts +4 -0
- package/dist/backend/util/JWKS.js +40 -0
- package/dist/bundled.d.ts +76 -21
- package/dist/chunks/{chunk-HXXBAY3B.js → chunk-YCQR5GRT.js} +1 -1
- package/dist/cli/Mcp.js +1 -1
- package/dist/cli/Serve.d.ts +1 -0
- package/dist/cli/Serve.js +5 -2
- package/dist/cli/bin.js +3 -2
- package/dist/cli/serve/DevServer.d.ts +2 -0
- package/dist/cli/serve/DevServer.js +14 -2
- package/dist/cli/serve/mcp/DevMcp.js +1 -1
- package/dist/cloud/CloudHandshake.js +9 -46
- package/dist/cloud/CloudRemote.js +1 -1
- package/dist/components/Alert.js +4 -4
- package/dist/components/Badge.js +4 -4
- package/dist/components/Breadcrumbs.js +7 -7
- package/dist/components/Calendar.js +19 -19
- package/dist/components/Checkbox.js +7 -7
- package/dist/components/Code.js +2 -2
- package/dist/components/Collapsible.js +4 -4
- package/dist/components/ComboBox.js +6 -6
- package/dist/components/Command.js +9 -9
- package/dist/components/ContentCard.js +21 -21
- package/dist/components/ContentGrid.js +9 -9
- package/dist/components/DataList.js +1 -1
- package/dist/components/Dialog.js +6 -6
- package/dist/components/DropZone.js +2 -2
- package/dist/components/DropdownMenu.js +7 -7
- package/dist/components/Empty.js +4 -4
- package/dist/components/Field.js +2 -2
- package/dist/components/List.js +27 -27
- package/dist/components/MediaPreview.js +5 -5
- package/dist/components/MultipleSelect.js +20 -20
- package/dist/components/NavRail.js +3 -3
- package/dist/components/NumberField.js +4 -4
- package/dist/components/Page.js +6 -6
- package/dist/components/PreviewFrame.js +7 -7
- package/dist/components/RadioGroup.js +7 -7
- package/dist/components/SearchField.js +3 -3
- package/dist/components/Select.js +10 -10
- package/dist/components/Sidebar.js +5 -5
- package/dist/components/SortableList.js +14 -14
- package/dist/components/Spinner.js +4 -4
- package/dist/components/Surface.js +3 -3
- package/dist/components/Switch.js +3 -3
- package/dist/components/Table.js +41 -41
- package/dist/components/Tabs.js +2 -2
- package/dist/components/TagGroup.js +6 -6
- package/dist/components/TextField.js +7 -7
- package/dist/components/TimeField.js +5 -5
- package/dist/components/Tooltip.js +2 -2
- package/dist/components/Tree.js +28 -28
- package/dist/components/internal/DateControl.js +7 -7
- package/dist/components/internal/ListBoxOption.js +9 -9
- package/dist/components/internal/PopoverSurface.js +2 -2
- package/dist/components/internal/SelectionCheckbox.js +3 -3
- package/dist/components/internal/ToggleButton.js +4 -4
- package/dist/core/Role.d.ts +4 -5
- package/dist/core/Role.js +55 -54
- package/dist/core/Scope.d.ts +2 -0
- package/dist/core/Scope.js +36 -2
- package/dist/core/Type.d.ts +7 -4
- package/dist/core/Type.js +5 -1
- package/dist/core/Validation.js +3 -3
- package/dist/core/media/CompressPdf.browser.js +1 -1
- package/dist/core/media/CompressPdf.js +1 -1
- package/dist/core/media/CreatePreview.browser.js +1 -1
- package/dist/core/util/JWT.d.ts +1 -1
- package/dist/dashboard/app/ActivityStatus.js +11 -11
- package/dist/dashboard/app/AuthView.js +5 -5
- package/dist/dashboard/app/CompactField.js +20 -20
- package/dist/dashboard/app/CroppedImage.js +3 -3
- package/dist/dashboard/app/DashboardErrorBoundary.js +3 -3
- package/dist/dashboard/app/EntryFields.js +2 -2
- package/dist/dashboard/app/EntryHeader.js +7 -7
- package/dist/dashboard/app/EntryReferences.js +4 -4
- package/dist/dashboard/app/EntrySidebar.js +17 -17
- package/dist/dashboard/app/EntrySidebarPreview.js +1 -1
- package/dist/dashboard/app/EntryValidationModal.js +3 -3
- package/dist/dashboard/app/Explorer.js +36 -43
- package/dist/dashboard/app/ExplorerBatchActions.js +4 -4
- package/dist/dashboard/app/ExplorerControls.js +3 -3
- package/dist/dashboard/app/ExplorerList.js +5 -5
- package/dist/dashboard/app/ExplorerModal.js +3 -3
- package/dist/dashboard/app/ExplorerPickerContent.d.ts +1 -1
- package/dist/dashboard/app/ExplorerPickerContent.js +17 -3
- package/dist/dashboard/app/ExplorerTable.js +8 -8
- package/dist/dashboard/app/ImageEditor.js +27 -27
- package/dist/dashboard/app/ImagePicker.js +3 -2
- package/dist/dashboard/app/LinkPicker.js +7 -6
- package/dist/dashboard/app/LocaleMenu.js +7 -7
- package/dist/dashboard/app/MediaPreviewCell.js +2 -2
- package/dist/dashboard/app/OverviewCell.js +8 -8
- package/dist/dashboard/app/PendingUploadsView.js +2 -2
- package/dist/dashboard/app/SidebarTree.js +26 -28
- package/dist/dashboard/app/WorkspaceMenu.js +8 -8
- package/dist/dashboard/app/WorkspaceRoots.js +10 -10
- package/dist/dashboard/app/editor/FileEditor.js +18 -18
- package/dist/dashboard/app/modals/CreateEntry.js +3 -3
- package/dist/dashboard/app/pages/EntryPage.js +10 -10
- package/dist/dashboard/app/pages/SplashPage.js +37 -37
- package/dist/dashboard/app/pages/UsersPage.js +10 -10
- package/dist/dashboard/app/ui/DashboardModal.js +17 -17
- package/dist/dashboard/atoms/config.d.ts +1 -1
- package/dist/dashboard/atoms/entry.d.ts +11 -4
- package/dist/dashboard/atoms/entry.js +103 -86
- package/dist/dashboard/atoms/entryTable.js +2 -1
- package/dist/dashboard/atoms/explorer.d.ts +17 -2
- package/dist/dashboard/atoms/explorer.js +38 -16
- package/dist/dashboard/atoms/link.d.ts +2 -0
- package/dist/dashboard/atoms/link.js +6 -3
- package/dist/dashboard/atoms/root.d.ts +15 -3
- package/dist/dashboard/atoms/root.js +111 -52
- package/dist/database/EntryTransaction.js +17 -6
- package/dist/field/code/CodeField.view.js +15 -15
- package/dist/field/link/LinkField.view.js +36 -35
- package/dist/field/list/ListField.view.js +4 -4
- package/dist/field/metadata/MetadataField.view.js +17 -17
- package/dist/field/richtext/RichTextExtensions.js +33 -33
- package/dist/field/richtext/RichTextField.view.js +5 -5
- package/docs/deploy.md +1 -1
- package/docs/roles-permissions.md +7 -7
- package/llms-full.txt +8 -8
- package/package.json +1 -1
- /package/dist/chunks/{dist-UDQZOKRG.js → dist-OU2RRDGP.js} +0 -0
package/dist/.mcp/later/dev.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"url":"http://localhost:1","pid":
|
|
1
|
+
{"url":"http://localhost:1","pid":3065}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"url":"http://localhost:
|
|
1
|
+
{"url":"http://localhost:38409","pid":3065}
|
|
@@ -67,8 +67,6 @@ function createHandlerWithDatabase(input, openGeneratedDatabase) {
|
|
|
67
67
|
return failure(400, "Expected JSON");
|
|
68
68
|
}
|
|
69
69
|
if (!Array.isArray(mutations)) return failure(400, "Expected mutations");
|
|
70
|
-
const adjusted = await options.beforeCommit?.({ mutations });
|
|
71
|
-
if (adjusted) mutations = [...adjusted];
|
|
72
70
|
const devServer = new Client({
|
|
73
71
|
config,
|
|
74
72
|
url: context.handlerUrl.href,
|
|
@@ -80,15 +78,19 @@ function createHandlerWithDatabase(input, openGeneratedDatabase) {
|
|
|
80
78
|
}
|
|
81
79
|
});
|
|
82
80
|
let sha;
|
|
81
|
+
let user;
|
|
83
82
|
try {
|
|
84
|
-
;
|
|
83
|
+
user = await devServer.user();
|
|
84
|
+
if (!user) return failure(401, "Unauthorized");
|
|
85
|
+
const adjusted = await options.beforeCommit?.({ mutations, user });
|
|
86
|
+
if (adjusted) mutations = [...adjusted];
|
|
85
87
|
({ sha } = await devServer.mutate(mutations));
|
|
86
88
|
} catch (error) {
|
|
87
89
|
if (error instanceof HttpError) return failure(error.code, error.message);
|
|
88
90
|
throw error;
|
|
89
91
|
}
|
|
90
92
|
try {
|
|
91
|
-
await afterCommit({ mutations, sha });
|
|
93
|
+
await afterCommit({ mutations, sha, user });
|
|
92
94
|
} catch (error) {
|
|
93
95
|
console.error("Alinea afterCommit hook failed", error);
|
|
94
96
|
}
|
|
@@ -2,12 +2,15 @@ import type { CMS } from 'alinea/core/CMS';
|
|
|
2
2
|
import type { AuthedContext, RemoteConnection, RequestContext } from 'alinea/core/Connection';
|
|
3
3
|
import type { LocalStore } from 'alinea/core/db/LocalStore';
|
|
4
4
|
import type { Mutation } from 'alinea/core/db/Mutation';
|
|
5
|
+
import type { User } from 'alinea/core/User';
|
|
5
6
|
export interface Handler {
|
|
6
7
|
(request: Request, context: RequestContext): Promise<Response>;
|
|
7
8
|
}
|
|
8
9
|
export type HookResponse<T = void> = void | T | Promise<void | T>;
|
|
9
10
|
export interface BeforeCommitContext {
|
|
10
11
|
mutations: ReadonlyArray<Mutation>;
|
|
12
|
+
/** The user committing the mutations */
|
|
13
|
+
user: User;
|
|
11
14
|
}
|
|
12
15
|
export interface AfterCommitContext extends BeforeCommitContext {
|
|
13
16
|
sha: string;
|
package/dist/backend/Handler.js
CHANGED
|
@@ -273,7 +273,10 @@ function createHandler({
|
|
|
273
273
|
const policy = await user.policy;
|
|
274
274
|
let mutations = await body;
|
|
275
275
|
await local.syncWith(cnx);
|
|
276
|
-
const adjusted = await hooks.beforeCommit?.({
|
|
276
|
+
const adjusted = await hooks.beforeCommit?.({
|
|
277
|
+
mutations,
|
|
278
|
+
user: user.claims
|
|
279
|
+
});
|
|
277
280
|
if (adjusted) mutations = adjusted;
|
|
278
281
|
const attempt = async (retry = 0) => {
|
|
279
282
|
if (retry > 0) await local.syncWith(cnx);
|
|
@@ -297,10 +300,7 @@ function createHandler({
|
|
|
297
300
|
};
|
|
298
301
|
const sha = await attempt();
|
|
299
302
|
try {
|
|
300
|
-
await hooks.afterCommit?.({
|
|
301
|
-
mutations,
|
|
302
|
-
sha
|
|
303
|
-
});
|
|
303
|
+
await hooks.afterCommit?.({ mutations, sha, user: user.claims });
|
|
304
304
|
} catch (error) {
|
|
305
305
|
console.error("Alinea afterCommit hook failed", error);
|
|
306
306
|
}
|
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
parse
|
|
3
3
|
} from "../../chunks/chunk-BYJRQTFR.js";
|
|
4
|
-
import {
|
|
5
|
-
PLazy
|
|
6
|
-
} from "../../chunks/chunk-IKINPSS5.js";
|
|
7
4
|
import "../../chunks/chunk-CQ52GEHY.js";
|
|
8
5
|
|
|
9
6
|
// src/backend/api/OAuth2.ts
|
|
10
7
|
import { AuthResultType } from "alinea/cloud/AuthResult";
|
|
11
8
|
import { Config } from "alinea/core/Config";
|
|
12
|
-
import { HttpError } from "alinea/core/HttpError";
|
|
9
|
+
import { ErrorCode, HttpError } from "alinea/core/HttpError";
|
|
13
10
|
import { createId } from "alinea/core/Id";
|
|
14
11
|
import { outcome } from "alinea/core/Outcome";
|
|
15
12
|
import { assert } from "alinea/core/util/Assert";
|
|
@@ -459,6 +456,7 @@ import {
|
|
|
459
456
|
MissingCredentialsError
|
|
460
457
|
} from "../Auth.js";
|
|
461
458
|
import { router } from "../router/Router.js";
|
|
459
|
+
import { jwks } from "../util/JWKS.js";
|
|
462
460
|
var COOKIE_VERIFIER = "alinea.cv";
|
|
463
461
|
var COOKIE_ACCESS_TOKEN = "alinea.at";
|
|
464
462
|
var COOKIE_REFRESH_TOKEN = "alinea.rt";
|
|
@@ -466,7 +464,7 @@ var OAuth2 = class {
|
|
|
466
464
|
#context;
|
|
467
465
|
#config;
|
|
468
466
|
#client;
|
|
469
|
-
#
|
|
467
|
+
#jwksUri;
|
|
470
468
|
#validateClaims;
|
|
471
469
|
constructor(context, config, options) {
|
|
472
470
|
assert(
|
|
@@ -489,19 +487,16 @@ var OAuth2 = class {
|
|
|
489
487
|
return response;
|
|
490
488
|
})
|
|
491
489
|
});
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
}
|
|
503
|
-
};
|
|
504
|
-
this.#jwks = PLazy.from(loadJwks);
|
|
490
|
+
this.#jwksUri = options.jwksUri;
|
|
491
|
+
}
|
|
492
|
+
async #key(token) {
|
|
493
|
+
const { header } = decode(token);
|
|
494
|
+
const keys = await jwks(this.#jwksUri, header.kid).catch((cause) => {
|
|
495
|
+
throw new HttpError(ErrorCode.ServiceUnavailable, "Remote unavailable", {
|
|
496
|
+
cause
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
return selectKey(keys, header);
|
|
505
500
|
}
|
|
506
501
|
get #redirectUri() {
|
|
507
502
|
const url = new URL(this.#context.handlerUrl);
|
|
@@ -612,11 +607,10 @@ var OAuth2 = class {
|
|
|
612
607
|
[COOKIE_ACCESS_TOKEN]: accessToken,
|
|
613
608
|
[COOKIE_REFRESH_TOKEN]: refreshToken
|
|
614
609
|
} = parse(cookieHeader);
|
|
615
|
-
const jwks = await this.#jwks;
|
|
616
610
|
try {
|
|
617
611
|
if (!accessToken)
|
|
618
612
|
throw new MissingCredentialsError("Missing access token cookie");
|
|
619
|
-
const key =
|
|
613
|
+
const key = await this.#key(accessToken);
|
|
620
614
|
const user = await verify(accessToken, key);
|
|
621
615
|
await this.#validateClaims(user);
|
|
622
616
|
assert(user.exp, "Missing exp claim in access token");
|
|
@@ -626,13 +620,15 @@ var OAuth2 = class {
|
|
|
626
620
|
return { ...ctx, user, token: accessToken };
|
|
627
621
|
} catch (error) {
|
|
628
622
|
if (!refreshToken) throw error;
|
|
623
|
+
if (error instanceof HttpError && error.code === ErrorCode.ServiceUnavailable)
|
|
624
|
+
throw error;
|
|
629
625
|
const token = { accessToken, refreshToken, expiresAt: null };
|
|
630
626
|
const newToken = await this.#client.refreshToken(token).catch((cause) => {
|
|
631
627
|
throw new InvalidCredentialsError("Failed to refresh token", {
|
|
632
628
|
cause: [cause, error]
|
|
633
629
|
});
|
|
634
630
|
});
|
|
635
|
-
const key =
|
|
631
|
+
const key = await this.#key(newToken.accessToken);
|
|
636
632
|
const user = await verify(newToken.accessToken, key, {
|
|
637
633
|
clockTolerance: 30
|
|
638
634
|
}).catch((cause) => {
|
|
@@ -653,13 +649,12 @@ var OAuth2 = class {
|
|
|
653
649
|
}
|
|
654
650
|
}
|
|
655
651
|
};
|
|
656
|
-
function selectKey(
|
|
657
|
-
const { header } = decode(token);
|
|
652
|
+
function selectKey(jwks2, header) {
|
|
658
653
|
const kid = header.kid;
|
|
659
654
|
const alg = header.alg;
|
|
660
655
|
if (!kid) {
|
|
661
|
-
if (
|
|
662
|
-
const key2 =
|
|
656
|
+
if (jwks2.length === 1) {
|
|
657
|
+
const key2 = jwks2[0];
|
|
663
658
|
if (key2.alg && alg && key2.alg !== alg) {
|
|
664
659
|
throw new Error(
|
|
665
660
|
`Algorithm mismatch: token uses ${alg}, but key specifies ${key2.alg}`
|
|
@@ -671,7 +666,7 @@ function selectKey(jwks, token) {
|
|
|
671
666
|
'Token is missing "kid" header, and JWKS contains multiple keys'
|
|
672
667
|
);
|
|
673
668
|
}
|
|
674
|
-
const key =
|
|
669
|
+
const key = jwks2.find((k) => k.kid === kid);
|
|
675
670
|
if (!key) throw new Error(`No key found for kid: ${kid}`);
|
|
676
671
|
if (key.alg && alg && key.alg !== alg)
|
|
677
672
|
throw new Error(
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import "../../chunks/chunk-CQ52GEHY.js";
|
|
2
|
+
|
|
3
|
+
// src/backend/util/JWKS.ts
|
|
4
|
+
import { HttpError } from "alinea/core/HttpError";
|
|
5
|
+
import { isRecord } from "alinea/core/util/Objects";
|
|
6
|
+
var lifetime = 10 * 60 * 1e3;
|
|
7
|
+
var refreshInterval = 60 * 1e3;
|
|
8
|
+
var cache = /* @__PURE__ */ new Map();
|
|
9
|
+
async function jwks(uri, kid) {
|
|
10
|
+
const cached = cache.get(uri);
|
|
11
|
+
if (!cached || Date.now() >= cached.expires) return load(uri, cached);
|
|
12
|
+
const keys = await cached.keys;
|
|
13
|
+
if (!kid || keys.some((key) => key.kid === kid)) return keys;
|
|
14
|
+
const current = cache.get(uri);
|
|
15
|
+
if (current && current !== cached) return current.keys;
|
|
16
|
+
if (Date.now() - cached.loaded < refreshInterval) return keys;
|
|
17
|
+
return load(uri, cached);
|
|
18
|
+
}
|
|
19
|
+
function load(uri, previous) {
|
|
20
|
+
const fetched = fetch(uri).then(async (response) => {
|
|
21
|
+
if (!response.ok) throw new HttpError(response.status);
|
|
22
|
+
const body = await response.json();
|
|
23
|
+
if (!isRecord(body) || !Array.isArray(body.keys))
|
|
24
|
+
throw new Error("Invalid signing keys");
|
|
25
|
+
return body.keys.filter(isKey);
|
|
26
|
+
});
|
|
27
|
+
const keys = previous ? fetched.catch(() => previous.keys) : fetched;
|
|
28
|
+
const entry = { keys, expires: Date.now() + lifetime, loaded: Date.now() };
|
|
29
|
+
cache.set(uri, entry);
|
|
30
|
+
keys.catch(() => {
|
|
31
|
+
if (cache.get(uri) === entry) cache.delete(uri);
|
|
32
|
+
});
|
|
33
|
+
return fetched;
|
|
34
|
+
}
|
|
35
|
+
function isKey(value) {
|
|
36
|
+
return isRecord(value) && typeof value.kid === "string";
|
|
37
|
+
}
|
|
38
|
+
export {
|
|
39
|
+
jwks
|
|
40
|
+
};
|
package/dist/bundled.d.ts
CHANGED
|
@@ -601,12 +601,15 @@ declare module 'alinea/backend/Handler' {
|
|
|
601
601
|
import type { AuthedContext, RemoteConnection, RequestContext } from 'alinea/core/Connection';
|
|
602
602
|
import type { LocalStore } from 'alinea/core/db/LocalStore';
|
|
603
603
|
import type { Mutation } from 'alinea/core/db/Mutation';
|
|
604
|
+
import type { User } from 'alinea/core/User';
|
|
604
605
|
export interface Handler {
|
|
605
606
|
(request: Request, context: RequestContext): Promise<Response>;
|
|
606
607
|
}
|
|
607
608
|
export type HookResponse<T = void> = void | T | Promise<void | T>;
|
|
608
609
|
export interface BeforeCommitContext {
|
|
609
610
|
mutations: ReadonlyArray<Mutation>;
|
|
611
|
+
/** The user committing the mutations */
|
|
612
|
+
user: User;
|
|
610
613
|
}
|
|
611
614
|
export interface AfterCommitContext extends BeforeCommitContext {
|
|
612
615
|
sha: string;
|
|
@@ -881,6 +884,15 @@ declare module 'alinea/backend/util/JsonPatch' {
|
|
|
881
884
|
|
|
882
885
|
}
|
|
883
886
|
|
|
887
|
+
declare module 'alinea/backend/util/JWKS' {
|
|
888
|
+
|
|
889
|
+
export interface WebKey extends JsonWebKey {
|
|
890
|
+
kid: string;
|
|
891
|
+
}
|
|
892
|
+
export function jwks(uri: string, kid?: string): Promise<Array<WebKey>>;
|
|
893
|
+
|
|
894
|
+
}
|
|
895
|
+
|
|
884
896
|
declare module 'alinea/backend/util/JWTPreviews' {
|
|
885
897
|
|
|
886
898
|
import type { Previews } from 'alinea/backend/Previews';
|
|
@@ -1204,6 +1216,7 @@ declare module 'alinea/cli/Serve' {
|
|
|
1204
1216
|
buildOptions?: BuildOptions;
|
|
1205
1217
|
alineaDev?: boolean;
|
|
1206
1218
|
production?: boolean;
|
|
1219
|
+
roles?: Array<string>;
|
|
1207
1220
|
onAfterGenerate?: (env?: Record<string, string>) => void;
|
|
1208
1221
|
}
|
|
1209
1222
|
export function serve(options: ServeOptions): Promise<void>;
|
|
@@ -1254,6 +1267,8 @@ declare module 'alinea/cli/serve/DevServer' {
|
|
|
1254
1267
|
buildOptions?: BuildOptions;
|
|
1255
1268
|
alineaDev?: boolean;
|
|
1256
1269
|
production?: boolean;
|
|
1270
|
+
/** Roles of the local user, instead of admin */
|
|
1271
|
+
roles?: Array<string>;
|
|
1257
1272
|
apiKey?: string;
|
|
1258
1273
|
dashboardUrl: Promise<string>;
|
|
1259
1274
|
onAfterGenerate?: (message: string, config: Config, databasePath: string) => void;
|
|
@@ -7111,9 +7126,8 @@ declare module 'alinea/core/Role' {
|
|
|
7111
7126
|
archive: boolean;
|
|
7112
7127
|
upload: boolean;
|
|
7113
7128
|
/**
|
|
7114
|
-
*
|
|
7115
|
-
*
|
|
7116
|
-
* roles keep type checking; it will be removed in a future major version.
|
|
7129
|
+
* Find entries in pickers, for example to link to them, without opening
|
|
7130
|
+
* them in the dashboard. Allowing `read` allows `explore` as well.
|
|
7117
7131
|
*/
|
|
7118
7132
|
explore: boolean;
|
|
7119
7133
|
manageMembers: boolean;
|
|
@@ -7149,12 +7163,13 @@ declare module 'alinea/core/Role' {
|
|
|
7149
7163
|
constructor(acl?: ACL);
|
|
7150
7164
|
get(resource: string): number;
|
|
7151
7165
|
equals(that: ACL): boolean;
|
|
7166
|
+
resolve(resource?: Resource): number;
|
|
7152
7167
|
}
|
|
7153
7168
|
export class Policy {
|
|
7154
7169
|
#private;
|
|
7155
7170
|
static ALLOW_ALL: Policy;
|
|
7156
7171
|
static ALLOW_NONE: Policy;
|
|
7157
|
-
protected
|
|
7172
|
+
protected acls: Array<ACL>;
|
|
7158
7173
|
constructor(root?: Permission);
|
|
7159
7174
|
static from(policy: Policy): Policy;
|
|
7160
7175
|
equals(that: Policy): boolean;
|
|
@@ -7171,7 +7186,6 @@ declare module 'alinea/core/Role' {
|
|
|
7171
7186
|
canPublish(resource?: Resource): boolean;
|
|
7172
7187
|
canArchive(resource?: Resource): boolean;
|
|
7173
7188
|
canUpload(resource?: Resource): boolean;
|
|
7174
|
-
/** @deprecated `explore` is never enforced, use `canRead` instead. */
|
|
7175
7189
|
canExplore(resource?: Resource): boolean;
|
|
7176
7190
|
canManageMembers(): boolean;
|
|
7177
7191
|
canAll(resource?: Resource): boolean;
|
|
@@ -7325,6 +7339,8 @@ declare module 'alinea/core/Scope' {
|
|
|
7325
7339
|
constructor(config: Config);
|
|
7326
7340
|
workspaceOf(root: HasRoot): HasWorkspace;
|
|
7327
7341
|
keyOf(entity: Entity): string;
|
|
7342
|
+
/** The type of a field and its path in there, such as `seo.title` */
|
|
7343
|
+
fieldOf(field: Field): [type: string, path: string] | undefined;
|
|
7328
7344
|
locationOf(entity: Entity): string[] | undefined;
|
|
7329
7345
|
nameOf(entity: Entity): string | undefined;
|
|
7330
7346
|
/** Configured spellings of a locale, matched case-insensitively. */
|
|
@@ -8029,6 +8045,7 @@ declare module 'alinea/core/Type' {
|
|
|
8029
8045
|
function childrenOrder(type: Type): OrderBy | Array<OrderBy> | undefined;
|
|
8030
8046
|
function isHidden(type: Type): boolean;
|
|
8031
8047
|
function isCollapsed(type: Type): boolean;
|
|
8048
|
+
function expands(type: Type): boolean;
|
|
8032
8049
|
function searchableText(type: Type, value: any): string;
|
|
8033
8050
|
function anchors(type: Type, value: Record<string, unknown>, path?: Array<string>, labels?: Array<string>): Array<EntryAnchorTarget>;
|
|
8034
8051
|
function references(type: Type, value: Record<string, unknown>, path?: Array<string>, labels?: Array<string>): Array<EntryReferenceTarget>;
|
|
@@ -8066,11 +8083,13 @@ declare module 'alinea/core/Type' {
|
|
|
8066
8083
|
/** Entries do not show up in the sidebar content tree */
|
|
8067
8084
|
hidden?: true;
|
|
8068
8085
|
/**
|
|
8069
|
-
*
|
|
8070
|
-
* they
|
|
8071
|
-
*
|
|
8086
|
+
* How entries show their children in the sidebar content tree. By default
|
|
8087
|
+
* they expand when the entry is opened. With `true` they stay collapsed
|
|
8088
|
+
* until the arrow is clicked, useful for entries with many children that
|
|
8089
|
+
* are browsed in their overview instead. With `false` they are expanded
|
|
8090
|
+
* from the start.
|
|
8072
8091
|
*/
|
|
8073
|
-
collapsed?:
|
|
8092
|
+
collapsed?: boolean;
|
|
8074
8093
|
/** An icon (React component) to represent this type in the dashboard */
|
|
8075
8094
|
icon?: ComponentType;
|
|
8076
8095
|
/** A React component used to view an entry of this type in the dashboard */
|
|
@@ -8529,7 +8548,7 @@ declare module 'alinea/core/util/JWT' {
|
|
|
8529
8548
|
};
|
|
8530
8549
|
};
|
|
8531
8550
|
type Algorithm = keyof typeof algorithms;
|
|
8532
|
-
type JWTHeader = {
|
|
8551
|
+
export type JWTHeader = {
|
|
8533
8552
|
alg: Algorithm;
|
|
8534
8553
|
kid?: string;
|
|
8535
8554
|
typ?: string;
|
|
@@ -9609,7 +9628,7 @@ declare module 'alinea/dashboard/app/ExplorerPickerContent' {
|
|
|
9609
9628
|
* The explorer of a picker opened at `location`, it starts over when the
|
|
9610
9629
|
* location or condition changes
|
|
9611
9630
|
*/
|
|
9612
|
-
export function usePickerExplorer(options: ExplorerOptions,
|
|
9631
|
+
export function usePickerExplorer(options: ExplorerOptions, requested: ExplorerLocation, initialView: ExplorerView): {
|
|
9613
9632
|
explorer: import("alinea/dashboard/atoms/explorer").ExplorerAtoms;
|
|
9614
9633
|
tree: (root: import("alinea/dashboard/atoms/root").RootAtoms, locale: string | null, location?: ExplorerLocation | undefined) => import("alinea/dashboard/atoms/root").TreeAtoms;
|
|
9615
9634
|
};
|
|
@@ -10258,7 +10277,7 @@ declare module 'alinea/dashboard/atoms/config' {
|
|
|
10258
10277
|
overview?: import("alinea/types").OverviewOptions;
|
|
10259
10278
|
orderChildrenBy?: import("alinea/core/OrderBy").OrderBy | Array<import("alinea/core/OrderBy").OrderBy>;
|
|
10260
10279
|
hidden?: true;
|
|
10261
|
-
collapsed?:
|
|
10280
|
+
collapsed?: boolean;
|
|
10262
10281
|
icon?: ComponentType;
|
|
10263
10282
|
view?: import("alinea/core/View").View<{
|
|
10264
10283
|
type: import("alinea/types").Type;
|
|
@@ -10533,6 +10552,7 @@ declare module 'alinea/dashboard/atoms/entry' {
|
|
|
10533
10552
|
import type { EntryReference } from 'alinea/core/db/EntryReference';
|
|
10534
10553
|
import { Entry, EntryStatus } from 'alinea/core/Entry';
|
|
10535
10554
|
import type { Order } from 'alinea/core/Graph';
|
|
10555
|
+
import { Permission } from 'alinea/core/Role';
|
|
10536
10556
|
import { type EntryDefaultView } from 'alinea/core/Type';
|
|
10537
10557
|
import { type FieldValidationError } from 'alinea/core/Validation';
|
|
10538
10558
|
import { Atom, Getter } from 'jotai';
|
|
@@ -10725,7 +10745,8 @@ declare module 'alinea/dashboard/atoms/entry' {
|
|
|
10725
10745
|
view: import("jotai").WritableAtom<EntryDefaultView, [next: EntryDefaultView], void>;
|
|
10726
10746
|
locales: (locale: string | null) => EntryLocaleAtoms;
|
|
10727
10747
|
}
|
|
10728
|
-
|
|
10748
|
+
/** An entry with the versions the policy allows with the permission */
|
|
10749
|
+
export function entryAtoms(entryId: string, permission?: Permission): Atom<Promise<EntryAtoms>>;
|
|
10729
10750
|
/** The versions linking to an entry, that the user can read */
|
|
10730
10751
|
export const incomingReferencesAtoms: (targetId: string) => Atom<Promise<EntryReferences>>;
|
|
10731
10752
|
/** The versions linking to the entries, that the user can read */
|
|
@@ -10756,11 +10777,12 @@ declare module 'alinea/dashboard/atoms/entry' {
|
|
|
10756
10777
|
id: string;
|
|
10757
10778
|
locale: string | null;
|
|
10758
10779
|
}>(items: Array<Item>, locale: string | null): Array<Item>;
|
|
10759
|
-
export function loadTreeChildren(get: Getter, location: TreeChildrenLocation, locale: string | null, orderBy?: Order | Array<Order>): Promise<Array<TreeEntryAtoms>>;
|
|
10780
|
+
export function loadTreeChildren(get: Getter, location: TreeChildrenLocation, locale: string | null, permission: Permission, orderBy?: Order | Array<Order>): Promise<Array<TreeEntryAtoms>>;
|
|
10760
10781
|
export class TreeEntryAtoms {
|
|
10761
10782
|
#private;
|
|
10762
10783
|
readonly id: string;
|
|
10763
|
-
|
|
10784
|
+
readonly permission: Permission;
|
|
10785
|
+
constructor(id: string, permission: Permission);
|
|
10764
10786
|
ready: Atom<Promise<this>>;
|
|
10765
10787
|
raw: (locale: string | null) => Atom<Promise<Omit<TreeEntrySummary, "hasChildren">>>;
|
|
10766
10788
|
summary: (locale: string | null) => Atom<Promise<{
|
|
@@ -10779,7 +10801,11 @@ declare module 'alinea/dashboard/atoms/entry' {
|
|
|
10779
10801
|
parents: Atom<Promise<TreeEntryAtoms[]>>;
|
|
10780
10802
|
children: (locale: string | null) => Atom<Promise<TreeEntryAtoms[]>>;
|
|
10781
10803
|
}
|
|
10782
|
-
|
|
10804
|
+
/**
|
|
10805
|
+
* An entry of a tree that lists what the policy allows with the permission.
|
|
10806
|
+
* Pickers list what can be explored, the sidebar what can be read.
|
|
10807
|
+
*/
|
|
10808
|
+
export function treeEntryAtoms(id: string, permission?: Permission): TreeEntryAtoms;
|
|
10783
10809
|
export {};
|
|
10784
10810
|
|
|
10785
10811
|
}
|
|
@@ -10802,12 +10828,13 @@ declare module 'alinea/dashboard/atoms/entryTable' {
|
|
|
10802
10828
|
declare module 'alinea/dashboard/atoms/explorer' {
|
|
10803
10829
|
|
|
10804
10830
|
import type { DragTypes, DropTarget, Key } from 'alinea/components';
|
|
10831
|
+
import type { Config } from 'alinea/core/Config';
|
|
10805
10832
|
import { type EntryStatus } from 'alinea/core/Entry';
|
|
10806
10833
|
import type { EntryFields } from 'alinea/core/EntryFields';
|
|
10807
10834
|
import type { Filter } from 'alinea/core/Filter';
|
|
10808
10835
|
import type { GraphQuery } from 'alinea/core/Graph';
|
|
10809
10836
|
import type { OverviewSort } from 'alinea/core/Overview';
|
|
10810
|
-
import { type Policy } from 'alinea/core/Role';
|
|
10837
|
+
import { Permission, type Policy } from 'alinea/core/Role';
|
|
10811
10838
|
import type { RootData } from 'alinea/core/Root';
|
|
10812
10839
|
import { Type } from 'alinea/core/Type';
|
|
10813
10840
|
import { type Atom, type Getter, type PrimitiveAtom, type WritableAtom } from 'jotai';
|
|
@@ -10826,6 +10853,7 @@ declare module 'alinea/dashboard/atoms/explorer' {
|
|
|
10826
10853
|
workspace: string;
|
|
10827
10854
|
root: string;
|
|
10828
10855
|
}
|
|
10856
|
+
export function constrainLocation(location: ExplorerLocation, limits: Array<ExplorerLimitLocation> | undefined): ExplorerLocation;
|
|
10829
10857
|
export interface ExplorerOptions {
|
|
10830
10858
|
allowAllWorkspaces?: boolean;
|
|
10831
10859
|
autoSelectFirstItem?: boolean;
|
|
@@ -10841,6 +10869,11 @@ declare module 'alinea/dashboard/atoms/explorer' {
|
|
|
10841
10869
|
limitLocations?: Array<ExplorerLimitLocation>;
|
|
10842
10870
|
mode?: 'browse' | 'search';
|
|
10843
10871
|
nestedNavigation?: boolean;
|
|
10872
|
+
/**
|
|
10873
|
+
* Entries are listed when the policy allows this permission on them,
|
|
10874
|
+
* defaults to read. Pickers list what can be explored.
|
|
10875
|
+
*/
|
|
10876
|
+
permission?: Permission;
|
|
10844
10877
|
pickChildren?: boolean;
|
|
10845
10878
|
rootData?: Atom<RootData>;
|
|
10846
10879
|
/**
|
|
@@ -10931,6 +10964,8 @@ declare module 'alinea/dashboard/atoms/explorer' {
|
|
|
10931
10964
|
* same entries in the same way and share their scroll offset
|
|
10932
10965
|
*/
|
|
10933
10966
|
export function explorerScrollKey(page: ExplorerReadyPage): string;
|
|
10967
|
+
/** The locations an explorer can list: those of the picker the policy allows */
|
|
10968
|
+
export function explorerLocations(config: Config, policy: Policy, options: ExplorerOptions): Array<ExplorerLimitLocation>;
|
|
10934
10969
|
export interface ExplorerItemData {
|
|
10935
10970
|
active?: boolean;
|
|
10936
10971
|
createdAt?: number | null;
|
|
@@ -10985,7 +11020,7 @@ declare module 'alinea/dashboard/atoms/explorer' {
|
|
|
10985
11020
|
* Loads the thumbnails and the entries linked from the overview columns of
|
|
10986
11021
|
* the given explorer items in a single query
|
|
10987
11022
|
*/
|
|
10988
|
-
export function withLinkedEntries<Item extends ExplorerItemData>(get: Getter, overview: OverviewState, items: Array<Item>, thumbnails?: boolean): Promise<Array<Item>>;
|
|
11023
|
+
export function withLinkedEntries<Item extends ExplorerItemData>(get: Getter, overview: OverviewState, items: Array<Item>, permission?: Permission, thumbnails?: boolean): Promise<Array<Item>>;
|
|
10989
11024
|
/** The selected entry can be moved to another parent */
|
|
10990
11025
|
export function explorerItemCanMove(policy: Policy, item: ExplorerItemData): boolean;
|
|
10991
11026
|
/** The selected entry can be deleted, in the language it is listed in */
|
|
@@ -11079,6 +11114,7 @@ declare module 'alinea/dashboard/atoms/explorer' {
|
|
|
11079
11114
|
readonly supportsInlineExpansion: boolean;
|
|
11080
11115
|
readonly pickChildren: boolean;
|
|
11081
11116
|
readonly hasCondition: boolean;
|
|
11117
|
+
readonly permission: Permission;
|
|
11082
11118
|
readonly canSearchEverything: Atom<boolean>;
|
|
11083
11119
|
readonly searchesEverything: Atom<boolean>;
|
|
11084
11120
|
search: PrimitiveAtom<string> & {
|
|
@@ -11093,6 +11129,9 @@ declare module 'alinea/dashboard/atoms/explorer' {
|
|
|
11093
11129
|
sidebarExpandedKeys: PrimitiveAtom<Set<string>> & {
|
|
11094
11130
|
init: Set<string>;
|
|
11095
11131
|
};
|
|
11132
|
+
sidebarClosedKeys: PrimitiveAtom<Set<string>> & {
|
|
11133
|
+
init: Set<string>;
|
|
11134
|
+
};
|
|
11096
11135
|
/** The column the editor sorted by, undefined for the default order */
|
|
11097
11136
|
requestedSort: WritableAtom<OverviewSort | undefined, [
|
|
11098
11137
|
OverviewSort | undefined
|
|
@@ -11138,6 +11177,8 @@ declare module 'alinea/dashboard/atoms/explorer' {
|
|
|
11138
11177
|
init: null;
|
|
11139
11178
|
};
|
|
11140
11179
|
get limitLocations(): ExplorerLimitLocation[] | undefined;
|
|
11180
|
+
/** The locations this explorer lists */
|
|
11181
|
+
locations: Atom<ExplorerLimitLocation[]>;
|
|
11141
11182
|
uploadsInCurrentFolder: Atom<import("alinea/dashboard/boot/ActivityEvent").Activity[]>;
|
|
11142
11183
|
upload: WritableAtom<null, [files: ArrayLike<File> | Iterable<File>], Promise<void>> & {
|
|
11143
11184
|
init: null;
|
|
@@ -11213,6 +11254,8 @@ declare module 'alinea/dashboard/atoms/link' {
|
|
|
11213
11254
|
title: string;
|
|
11214
11255
|
}>;
|
|
11215
11256
|
anchors: Array<EntryAnchorTarget>;
|
|
11257
|
+
/** The editor can open the entry, not only link to it */
|
|
11258
|
+
readable: boolean;
|
|
11216
11259
|
}
|
|
11217
11260
|
export type LinkEntryState = {
|
|
11218
11261
|
state: 'loading';
|
|
@@ -11627,6 +11670,7 @@ declare module 'alinea/dashboard/atoms/ReactiveNode' {
|
|
|
11627
11670
|
declare module 'alinea/dashboard/atoms/root' {
|
|
11628
11671
|
|
|
11629
11672
|
import type { EntryStatus } from 'alinea/core/Entry';
|
|
11673
|
+
import { Permission } from 'alinea/core/Role';
|
|
11630
11674
|
import { type RootData, type RootI18n } from 'alinea/core/Root';
|
|
11631
11675
|
import { Schema } from 'alinea/core/Schema';
|
|
11632
11676
|
import { ExplorerAtoms } from 'alinea/dashboard/atoms/explorer';
|
|
@@ -11671,12 +11715,22 @@ declare module 'alinea/dashboard/atoms/root' {
|
|
|
11671
11715
|
interface TreeViewState {
|
|
11672
11716
|
expandedKeys: PrimitiveAtom<Set<string>>;
|
|
11673
11717
|
collapsedKeys: PrimitiveAtom<TreeCollapseState>;
|
|
11718
|
+
closedKeys: PrimitiveAtom<Set<string>>;
|
|
11674
11719
|
}
|
|
11675
11720
|
export class TreeAtoms {
|
|
11676
11721
|
#private;
|
|
11677
11722
|
expandedKeys: PrimitiveAtom<Set<string>>;
|
|
11678
11723
|
collapsedKeys: PrimitiveAtom<TreeCollapseState>;
|
|
11679
|
-
|
|
11724
|
+
/** Entries closed by hand, which stay closed when their type expands */
|
|
11725
|
+
closedKeys: PrimitiveAtom<Set<string>>;
|
|
11726
|
+
constructor(root: RootAtoms, locale: string | null, selectedKeys: Atom<Set<Key>>, viewState?: TreeViewState, permission?: Permission);
|
|
11727
|
+
/**
|
|
11728
|
+
* Applies what the user expanded and collapsed. Keys closed by hand are
|
|
11729
|
+
* remembered, so entries of types that expand from the start stay closed.
|
|
11730
|
+
*/
|
|
11731
|
+
expand: import("jotai").WritableAtom<null, [next: Set<string>], void> & {
|
|
11732
|
+
init: null;
|
|
11733
|
+
};
|
|
11680
11734
|
view: Atom<TreeView>;
|
|
11681
11735
|
snapshot: Atom<TreeSnapshot>;
|
|
11682
11736
|
items: Atom<RootTreeItem[]>;
|
|
@@ -11695,8 +11749,9 @@ declare module 'alinea/dashboard/atoms/root' {
|
|
|
11695
11749
|
readonly tree: (locale: string | null) => TreeAtoms;
|
|
11696
11750
|
explorer: ExplorerAtoms;
|
|
11697
11751
|
constructor(workspace: string, key: string);
|
|
11698
|
-
|
|
11699
|
-
|
|
11752
|
+
/** A tree that lists the entries the policy allows with the permission */
|
|
11753
|
+
createTree(locale: string | null, selectedKeys: Atom<Set<Key>>, state?: Partial<Pick<TreeViewState, 'expandedKeys' | 'closedKeys'>>, permission?: Permission): TreeAtoms;
|
|
11754
|
+
treeEntries(locale: string | null, permission?: Permission): Atom<Promise<TreeEntryAtoms[]>>;
|
|
11700
11755
|
/**
|
|
11701
11756
|
* The scroll offsets of the explorers of this root. The root explorer and
|
|
11702
11757
|
* the overview of an entry can list the same location, which then keeps its
|
package/dist/cli/Mcp.js
CHANGED
package/dist/cli/Serve.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export interface ServeOptions {
|
|
|
8
8
|
buildOptions?: BuildOptions;
|
|
9
9
|
alineaDev?: boolean;
|
|
10
10
|
production?: boolean;
|
|
11
|
+
roles?: Array<string>;
|
|
11
12
|
onAfterGenerate?: (env?: Record<string, string>) => void;
|
|
12
13
|
}
|
|
13
14
|
export declare function serve(options: ServeOptions): Promise<void>;
|
package/dist/cli/Serve.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
package_default
|
|
3
|
-
} from "../chunks/chunk-
|
|
3
|
+
} from "../chunks/chunk-YCQR5GRT.js";
|
|
4
4
|
import "../chunks/chunk-CQ52GEHY.js";
|
|
5
5
|
|
|
6
6
|
// src/cli/Serve.ts
|
|
@@ -34,6 +34,7 @@ async function serve(options) {
|
|
|
34
34
|
configFile: options.configFile,
|
|
35
35
|
alineaDev: options.alineaDev,
|
|
36
36
|
production: options.production,
|
|
37
|
+
roles: options.roles,
|
|
37
38
|
apiKey,
|
|
38
39
|
buildOptions: {
|
|
39
40
|
...buildOptions,
|
|
@@ -55,7 +56,9 @@ async function serve(options) {
|
|
|
55
56
|
`;
|
|
56
57
|
const showUrl = cmd === "dev" && !options.onAfterGenerate;
|
|
57
58
|
const connector = gray(showUrl ? "\u251C" : "\u2570");
|
|
58
|
-
const
|
|
59
|
+
const roles = options.roles ? gray(`\u251C Signed in as ${options.roles.join(", ")}
|
|
60
|
+
`) : "";
|
|
61
|
+
const details = `${roles}${connector} ${gray(msg)}
|
|
59
62
|
`;
|
|
60
63
|
const footer = showUrl ? `${gray("\u2570")} Local CMS: ${url}
|
|
61
64
|
|
package/dist/cli/bin.js
CHANGED
|
@@ -220,7 +220,7 @@ var lib_default2 = (e, t2) => new o(e, t2);
|
|
|
220
220
|
// package.json
|
|
221
221
|
var package_default = {
|
|
222
222
|
name: "alinea",
|
|
223
|
-
version: "2.0.0-preview.
|
|
223
|
+
version: "2.0.0-preview.11",
|
|
224
224
|
description: "Open source, Git-based headless CMS for Next.js",
|
|
225
225
|
keywords: [
|
|
226
226
|
"cms",
|
|
@@ -336,7 +336,7 @@ prog.version(package_default.version).command("init").describe("Initialize a new
|
|
|
336
336
|
ensureLibs(libs);
|
|
337
337
|
const { init } = await import("./Init.js");
|
|
338
338
|
return init(args);
|
|
339
|
-
}).command("dev").alias("serve").describe("Start a development dashboard").option("-c, --config", "Config file location").option("-d, --dir", "Root directory of the project").option("-p, --port", "Port to listen on").option("--production", "Use production backend").option("--dev", "Watch alinea sources").action(async (args) => {
|
|
339
|
+
}).command("dev").alias("serve").describe("Start a development dashboard").option("-c, --config", "Config file location").option("-d, --dir", "Root directory of the project").option("-p, --port", "Port to listen on").option("--production", "Use production backend").option("--dev", "Watch alinea sources").option("--role", "Sign in locally with this role, repeat for more").action(async (args) => {
|
|
340
340
|
ensureNode();
|
|
341
341
|
ensureLibs(libs);
|
|
342
342
|
ensureEnv(args.dir);
|
|
@@ -349,6 +349,7 @@ prog.version(package_default.version).command("init").describe("Initialize a new
|
|
|
349
349
|
cwd: args.dir,
|
|
350
350
|
onAfterGenerate,
|
|
351
351
|
configFile: args.config,
|
|
352
|
+
roles: args.role && [args.role].flat().filter((role) => typeof role === "string"),
|
|
352
353
|
cmd: "dev"
|
|
353
354
|
});
|
|
354
355
|
}).command("mcp").describe("Connect coding agents to the content of the running dev server").option("-c, --config", "Config file location").option("-d, --dir", "Root directory of the project").action(async (args) => {
|