@gogitcms/editor 0.23.0
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/app/index.html +38 -0
- package/app/src/App.tsx +2665 -0
- package/app/src/apollo.ts +119 -0
- package/app/src/auth.ts +248 -0
- package/app/src/config.ts +88 -0
- package/app/src/main.tsx +18 -0
- package/app/src/media.ts +151 -0
- package/app/src/navigation.tsx +321 -0
- package/app/src/plugins.ts +11 -0
- package/app/src/previewHost.tsx +263 -0
- package/app/src/previewToken.ts +68 -0
- package/app/src/queries.ts +591 -0
- package/app/src/virtual-cms-plugins.d.ts +6 -0
- package/app/vendor/analytics/src/__tests__/config.test.ts +91 -0
- package/app/vendor/analytics/src/config.ts +94 -0
- package/app/vendor/analytics/src/events.ts +56 -0
- package/app/vendor/analytics/src/index.ts +23 -0
- package/app/vendor/analytics/src/provider.tsx +196 -0
- package/app/vendor/design-system/src/ThemeProvider.tsx +86 -0
- package/app/vendor/design-system/src/__tests__/ApplyChangesModal.test.tsx +148 -0
- package/app/vendor/design-system/src/__tests__/BranchImport.test.tsx +46 -0
- package/app/vendor/design-system/src/__tests__/Button.test.tsx +45 -0
- package/app/vendor/design-system/src/__tests__/ChangeRequestSummary.test.tsx +57 -0
- package/app/vendor/design-system/src/__tests__/ContentBrowser.changes.test.tsx +611 -0
- package/app/vendor/design-system/src/__tests__/ContentBrowser.collab.test.tsx +322 -0
- package/app/vendor/design-system/src/__tests__/ContentBrowser.collabsync.test.tsx +264 -0
- package/app/vendor/design-system/src/__tests__/ContentBrowser.contentslot.test.tsx +53 -0
- package/app/vendor/design-system/src/__tests__/ContentBrowser.discriminator.test.tsx +142 -0
- package/app/vendor/design-system/src/__tests__/ContentBrowser.drafts.test.tsx +271 -0
- package/app/vendor/design-system/src/__tests__/ContentBrowser.fields.test.tsx +117 -0
- package/app/vendor/design-system/src/__tests__/ContentBrowser.media.test.tsx +140 -0
- package/app/vendor/design-system/src/__tests__/ContentBrowser.mixedcollab.test.tsx +63 -0
- package/app/vendor/design-system/src/__tests__/ContentBrowser.mixedvalues.test.tsx +38 -0
- package/app/vendor/design-system/src/__tests__/ContentBrowser.pagination.test.tsx +62 -0
- package/app/vendor/design-system/src/__tests__/ContentBrowser.previewtab.test.tsx +212 -0
- package/app/vendor/design-system/src/__tests__/ContentBrowser.reorder.test.tsx +45 -0
- package/app/vendor/design-system/src/__tests__/ContentBrowser.search.test.tsx +135 -0
- package/app/vendor/design-system/src/__tests__/ContentBrowser.selectvalue.test.tsx +185 -0
- package/app/vendor/design-system/src/__tests__/ContentBrowser.staged.test.tsx +132 -0
- package/app/vendor/design-system/src/__tests__/ContentBrowser.usermenu.test.tsx +56 -0
- package/app/vendor/design-system/src/__tests__/MediaBrowser.test.tsx +353 -0
- package/app/vendor/design-system/src/__tests__/MediaField.test.tsx +185 -0
- package/app/vendor/design-system/src/__tests__/Notifications.test.tsx +69 -0
- package/app/vendor/design-system/src/__tests__/Onboarding.test.tsx +287 -0
- package/app/vendor/design-system/src/__tests__/cssTokens.test.ts +201 -0
- package/app/vendor/design-system/src/__tests__/fieldComponents.test.ts +43 -0
- package/app/vendor/design-system/src/__tests__/reorder.test.ts +58 -0
- package/app/vendor/design-system/src/components/ApplyChangesModal.tsx +348 -0
- package/app/vendor/design-system/src/components/BranchImport.tsx +157 -0
- package/app/vendor/design-system/src/components/BranchMenu.tsx +192 -0
- package/app/vendor/design-system/src/components/Button.tsx +131 -0
- package/app/vendor/design-system/src/components/ChangeDetail.tsx +472 -0
- package/app/vendor/design-system/src/components/ChangeRequestSummary.tsx +173 -0
- package/app/vendor/design-system/src/components/CollabField.tsx +388 -0
- package/app/vendor/design-system/src/components/ContentBrowser.tsx +5073 -0
- package/app/vendor/design-system/src/components/Icon.tsx +28 -0
- package/app/vendor/design-system/src/components/Icon.web.tsx +31 -0
- package/app/vendor/design-system/src/components/Input.tsx +106 -0
- package/app/vendor/design-system/src/components/MediaBrowser.tsx +766 -0
- package/app/vendor/design-system/src/components/MediaField.tsx +670 -0
- package/app/vendor/design-system/src/components/MediaPreview.tsx +91 -0
- package/app/vendor/design-system/src/components/MediaPreview.web.tsx +169 -0
- package/app/vendor/design-system/src/components/NavRow.tsx +105 -0
- package/app/vendor/design-system/src/components/Notifications.tsx +301 -0
- package/app/vendor/design-system/src/components/Onboarding.tsx +751 -0
- package/app/vendor/design-system/src/components/ProjectMenu.tsx +124 -0
- package/app/vendor/design-system/src/components/Segment.tsx +87 -0
- package/app/vendor/design-system/src/components/Skeleton.tsx +216 -0
- package/app/vendor/design-system/src/components/Spinner.tsx +44 -0
- package/app/vendor/design-system/src/components/Text.tsx +85 -0
- package/app/vendor/design-system/src/components/documentDrafts.ts +213 -0
- package/app/vendor/design-system/src/components/layout.tsx +284 -0
- package/app/vendor/design-system/src/components/primitives.tsx +143 -0
- package/app/vendor/design-system/src/components/reorder.ts +40 -0
- package/app/vendor/design-system/src/fieldComponents.ts +63 -0
- package/app/vendor/design-system/src/icons.ts +102 -0
- package/app/vendor/design-system/src/index.ts +198 -0
- package/app/vendor/design-system/src/media.ts +229 -0
- package/app/vendor/design-system/src/theme.ts +116 -0
- package/app/vendor/design-system/src/web/Button.tsx +110 -0
- package/app/vendor/design-system/src/web/Icon.tsx +52 -0
- package/app/vendor/design-system/src/web/Input.tsx +39 -0
- package/app/vendor/design-system/src/web/index.ts +43 -0
- package/app/vendor/design-system/src/web/primitives.tsx +119 -0
- package/app/vendor/markdown-editor/src/MarkdownEditor.tsx +248 -0
- package/app/vendor/markdown-editor/src/Toolbar.tsx +157 -0
- package/app/vendor/markdown-editor/src/field.tsx +67 -0
- package/app/vendor/markdown-editor/src/flavors/commonmark/index.ts +27 -0
- package/app/vendor/markdown-editor/src/flavors/gfm/index.ts +30 -0
- package/app/vendor/markdown-editor/src/flavors/gfm/parser.ts +73 -0
- package/app/vendor/markdown-editor/src/flavors/gfm/schema.ts +70 -0
- package/app/vendor/markdown-editor/src/flavors/gfm/serializer.ts +82 -0
- package/app/vendor/markdown-editor/src/flavors/gfm/taskList.ts +39 -0
- package/app/vendor/markdown-editor/src/flavors/registry.ts +13 -0
- package/app/vendor/markdown-editor/src/flavors/shared/inputrules.ts +79 -0
- package/app/vendor/markdown-editor/src/flavors/shared/keymap.ts +89 -0
- package/app/vendor/markdown-editor/src/flavors/shared/placeholder.ts +24 -0
- package/app/vendor/markdown-editor/src/flavors/shared/plugins.ts +58 -0
- package/app/vendor/markdown-editor/src/flavors/types.ts +22 -0
- package/app/vendor/markdown-editor/src/formats/registry.ts +45 -0
- package/app/vendor/markdown-editor/src/icons.tsx +120 -0
- package/app/vendor/markdown-editor/src/index.ts +23 -0
- package/app/vendor/markdown-editor/src/theme.ts +249 -0
- package/app/vendor/markdown-editor/src/types.ts +58 -0
- package/app/vendor/plugin-sdk/src/__tests__/documentActions.test.ts +108 -0
- package/app/vendor/plugin-sdk/src/__tests__/loader.test.ts +75 -0
- package/app/vendor/plugin-sdk/src/__tests__/registry.test.ts +148 -0
- package/app/vendor/plugin-sdk/src/index.ts +33 -0
- package/app/vendor/plugin-sdk/src/loader.ts +45 -0
- package/app/vendor/plugin-sdk/src/navkeys.ts +17 -0
- package/app/vendor/plugin-sdk/src/react.tsx +155 -0
- package/app/vendor/plugin-sdk/src/registry.ts +337 -0
- package/app/vendor/plugin-sdk/src/types.ts +230 -0
- package/app/vendor/realtime/src/__tests__/pure.test.ts +58 -0
- package/app/vendor/realtime/src/__tests__/seeded-room.base64 +1 -0
- package/app/vendor/realtime/src/__tests__/seeding.test.ts +70 -0
- package/app/vendor/realtime/src/collab.ts +308 -0
- package/app/vendor/realtime/src/hooks.ts +57 -0
- package/app/vendor/realtime/src/index.ts +12 -0
- package/app/vendor/realtime/src/pure.ts +43 -0
- package/app/vite.config.mjs +59 -0
- package/bin/gogitcms-editor.mjs +168 -0
- package/npm-shrinkwrap.json +5696 -0
- package/package.json +74 -0
- package/src/commands/build.mjs +100 -0
- package/src/commands/dev.mjs +186 -0
- package/src/commands/init.mjs +144 -0
- package/src/commands/login.mjs +116 -0
- package/src/commands/status.mjs +68 -0
- package/src/lib/api.mjs +122 -0
- package/src/lib/appRoot.mjs +51 -0
- package/src/lib/baked.json +3 -0
- package/src/lib/config.mjs +108 -0
- package/src/lib/credentials.mjs +91 -0
- package/src/lib/defaults.mjs +26 -0
- package/src/lib/graphql.mjs +28 -0
- package/src/lib/localServer.mjs +206 -0
- package/src/lib/open.mjs +23 -0
- package/src/lib/plugins.mjs +164 -0
- package/src/lib/tokenBroker.mjs +85 -0
- package/src/tui/prompts.mjs +92 -0
- package/src/tui/screen.mjs +282 -0
- package/src/tui/theme.mjs +40 -0
package/package.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gogitcms/editor",
|
|
3
|
+
"version": "0.23.0",
|
|
4
|
+
"description": "Terminal UI for setting up and running the Go·Git CMS content editor",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"gogitcms-editor": "bin/gogitcms-editor.mjs"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/go-git-cms/gogitcms.git",
|
|
12
|
+
"directory": "apps/editor"
|
|
13
|
+
},
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=20"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"bin",
|
|
22
|
+
"src",
|
|
23
|
+
"app",
|
|
24
|
+
"npm-shrinkwrap.json"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"prepack": "node scripts/prepack.mjs",
|
|
28
|
+
"smoke": "node scripts/smoke.mjs",
|
|
29
|
+
"start": "node bin/gogitcms-editor.mjs"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@apollo/client": "^3.11.8",
|
|
33
|
+
"@handlewithcare/react-prosemirror": "^3.2.7",
|
|
34
|
+
"@react-navigation/native": "^7.3.11",
|
|
35
|
+
"@vitejs/plugin-react": "^4.3.1",
|
|
36
|
+
"graphql": "^16.9.0",
|
|
37
|
+
"graphql-ws": "^6.1.0",
|
|
38
|
+
"markdown-it": "^14.3.0",
|
|
39
|
+
"posthog-js": "^1.200.0",
|
|
40
|
+
"prosemirror-commands": "^1.7.1",
|
|
41
|
+
"prosemirror-dropcursor": "^1.8.3",
|
|
42
|
+
"prosemirror-gapcursor": "^1.4.1",
|
|
43
|
+
"prosemirror-history": "^1.5.0",
|
|
44
|
+
"prosemirror-inputrules": "^1.5.1",
|
|
45
|
+
"prosemirror-keymap": "^1.2.3",
|
|
46
|
+
"prosemirror-markdown": "^1.13.5",
|
|
47
|
+
"prosemirror-model": "^1.25.11",
|
|
48
|
+
"prosemirror-schema-list": "^1.5.1",
|
|
49
|
+
"prosemirror-state": "^1.4.4",
|
|
50
|
+
"prosemirror-tables": "^1.8.5",
|
|
51
|
+
"prosemirror-transform": "^1.12.0",
|
|
52
|
+
"prosemirror-view": "1.42.0",
|
|
53
|
+
"react": "^19.2.0",
|
|
54
|
+
"react-dom": "^19.2.0",
|
|
55
|
+
"react-native-web": "^0.21.2",
|
|
56
|
+
"react-reconciler": "0.33.0",
|
|
57
|
+
"terminal-kit": "^3.1.2",
|
|
58
|
+
"vite": "^5.4.2",
|
|
59
|
+
"y-prosemirror": "^1.2.12",
|
|
60
|
+
"y-protocols": "^1.0.6",
|
|
61
|
+
"y-websocket": "^2.1.0",
|
|
62
|
+
"yjs": "^13.6.20"
|
|
63
|
+
},
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"@gogitcms/frontend": "workspace:*"
|
|
66
|
+
},
|
|
67
|
+
"optionalDependencies": {
|
|
68
|
+
"@gogitcms/gitcms-local-darwin-arm64": "0.23.0",
|
|
69
|
+
"@gogitcms/gitcms-local-darwin-x64": "0.23.0",
|
|
70
|
+
"@gogitcms/gitcms-local-linux-arm64": "0.23.0",
|
|
71
|
+
"@gogitcms/gitcms-local-linux-x64": "0.23.0",
|
|
72
|
+
"@gogitcms/gitcms-local-win32-x64": "0.23.0"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// `gogitcms-editor build` — export the editor as a static SPA for self-hosting.
|
|
2
|
+
//
|
|
3
|
+
// The output is deliberately three files: index.html plus one JS and one CSS
|
|
4
|
+
// bundle. That is what a self-hoster can drop on any static host without
|
|
5
|
+
// thinking about chunk graphs or preload headers.
|
|
6
|
+
|
|
7
|
+
import fs from "node:fs/promises";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import * as config from "../lib/config.mjs";
|
|
10
|
+
import * as credentials from "../lib/credentials.mjs";
|
|
11
|
+
import { resolveAppRoot, viteConfigFile } from "../lib/appRoot.mjs";
|
|
12
|
+
import { PLUGIN_PEER_DEPS, cmsPluginsVite, projectAliases, resolvePlugins } from "../lib/plugins.mjs";
|
|
13
|
+
|
|
14
|
+
export async function build(screen, opts) {
|
|
15
|
+
const cfg = await config.load();
|
|
16
|
+
screen.header("BUILD");
|
|
17
|
+
if (!cfg) {
|
|
18
|
+
screen.fail("No cms.config.js found — run gogitcms-editor init first.");
|
|
19
|
+
return 1;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Plugins listed in cms.config are bundled into the SPA (resolved from the
|
|
23
|
+
// project directory), the same as the dev server does.
|
|
24
|
+
let resolvedPlugins;
|
|
25
|
+
try {
|
|
26
|
+
resolvedPlugins = resolvePlugins(cfg);
|
|
27
|
+
} catch (e) {
|
|
28
|
+
screen.fail(e.message);
|
|
29
|
+
return 1;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const apiUrl = credentials.normalizeApiUrl(opts.apiUrl || cfg.API_URL);
|
|
33
|
+
const outDir = path.resolve(opts.out || "dist");
|
|
34
|
+
const appRoot = resolveAppRoot();
|
|
35
|
+
// BASE_PATH in the config is the deployment's mount path; --base overrides it
|
|
36
|
+
// for a one-off build. It sets both the asset URLs and, via BASE_URL, the
|
|
37
|
+
// paths the SPA's router writes.
|
|
38
|
+
const base = config.basePathFor(cfg, opts.base);
|
|
39
|
+
|
|
40
|
+
screen.setDone(0, Boolean(await credentials.load(apiUrl)));
|
|
41
|
+
screen.setDone(1);
|
|
42
|
+
screen.setStatus({ mode: "RUN", hint: "building" });
|
|
43
|
+
await screen.stream([{ text: "transforming content…", cls: "dim" }]);
|
|
44
|
+
|
|
45
|
+
const started = Date.now();
|
|
46
|
+
const { build: viteBuild } = await import("vite");
|
|
47
|
+
try {
|
|
48
|
+
await viteBuild({
|
|
49
|
+
root: appRoot,
|
|
50
|
+
configFile: viteConfigFile(appRoot),
|
|
51
|
+
base,
|
|
52
|
+
// The bundle is built for one deployment, so its API target is baked in
|
|
53
|
+
// rather than left to a runtime global.
|
|
54
|
+
define: config.defineFor({ ...cfg, API_URL: apiUrl }),
|
|
55
|
+
resolve: { dedupe: PLUGIN_PEER_DEPS, alias: projectAliases() },
|
|
56
|
+
plugins: [cmsPluginsVite(resolvedPlugins)],
|
|
57
|
+
logLevel: "silent",
|
|
58
|
+
build: {
|
|
59
|
+
outDir,
|
|
60
|
+
emptyOutDir: true,
|
|
61
|
+
// One JS file and one CSS file, with stable names: no hashed chunk soup
|
|
62
|
+
// for a self-hoster to wire up.
|
|
63
|
+
cssCodeSplit: false,
|
|
64
|
+
rollupOptions: {
|
|
65
|
+
output: {
|
|
66
|
+
inlineDynamicImports: true,
|
|
67
|
+
entryFileNames: "app.js",
|
|
68
|
+
assetFileNames: "app.[ext]",
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
} catch (e) {
|
|
74
|
+
screen.fail(`Build failed — ${e.message}`);
|
|
75
|
+
return 1;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const files = await fs.readdir(outDir);
|
|
79
|
+
let bytes = 0;
|
|
80
|
+
for (const f of files) bytes += (await fs.stat(path.join(outDir, f))).size;
|
|
81
|
+
|
|
82
|
+
screen.setDone(2);
|
|
83
|
+
screen.setStatus({ mode: "DONE", hint: "ready to deploy" });
|
|
84
|
+
await screen.stream([
|
|
85
|
+
...files.sort().map((f) => ({ text: ` ${f}`, cls: "out" })),
|
|
86
|
+
{ text: `✓ ${files.length} files · ${(bytes / 1024 / 1024).toFixed(1)} MB`, cls: "ok" },
|
|
87
|
+
{ text: `✓ built in ${((Date.now() - started) / 1000).toFixed(2)}s → ${path.relative(process.cwd(), outDir) || outDir}`, cls: "ok" },
|
|
88
|
+
{ text: ` serve at ${base}`, cls: "dim" },
|
|
89
|
+
"",
|
|
90
|
+
// Deep links are client-side routes with no file behind them, so a host that
|
|
91
|
+
// 404s unknown paths breaks every reload and bookmark.
|
|
92
|
+
{ text: `Serve index.html for unknown paths under ${base} so deep links reload.`, cls: "dim" },
|
|
93
|
+
// The hosted SPA signs in with PKCE, and the server only returns an
|
|
94
|
+
// authorization code to a registered editor origin. Saying so here saves a
|
|
95
|
+
// confusing "invalid redirect_uri" on the first real deploy.
|
|
96
|
+
{ text: "Register this site's URL as an editor in the dashboard, or sign-in", cls: "dim" },
|
|
97
|
+
{ text: "will be refused at its new origin.", cls: "dim" },
|
|
98
|
+
]);
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// `gogitcms-editor dev` — run the editor locally against your CMS (step 03).
|
|
2
|
+
//
|
|
3
|
+
// The SPA normally signs in with PKCE, which requires its origin to be a
|
|
4
|
+
// registered editor deployment; a throwaway localhost port never is. The CLI
|
|
5
|
+
// already holds a session from `login`, so it brokers one instead: the dev
|
|
6
|
+
// server exposes /__gitcms/token, and the SPA takes its access token from there.
|
|
7
|
+
// See tokenBroker below for why that endpoint is safe to expose.
|
|
8
|
+
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import * as api from "../lib/api.mjs";
|
|
11
|
+
import * as config from "../lib/config.mjs";
|
|
12
|
+
import * as credentials from "../lib/credentials.mjs";
|
|
13
|
+
import { resolveAppRoot, viteConfigFile } from "../lib/appRoot.mjs";
|
|
14
|
+
import { openUrl } from "../lib/open.mjs";
|
|
15
|
+
import { TOKEN_PATH, localTokenBroker, tokenBroker } from "../lib/tokenBroker.mjs";
|
|
16
|
+
import * as localServer from "../lib/localServer.mjs";
|
|
17
|
+
import { PLUGIN_PEER_DEPS, cmsPluginsVite, pluginDirs, projectAliases, resolvePlugins } from "../lib/plugins.mjs";
|
|
18
|
+
|
|
19
|
+
export async function dev(screen, opts) {
|
|
20
|
+
const cfg = await config.load();
|
|
21
|
+
// Local mode is the one path that does not require a config: the working copy's
|
|
22
|
+
// go-git-cms.yml already says what the content is, and cms.config.mjs exists to
|
|
23
|
+
// point the SPA at a hosted API there isn't one of.
|
|
24
|
+
const local = Boolean(opts.local) || cfg?.MODE === "local";
|
|
25
|
+
if (!cfg && !local) {
|
|
26
|
+
screen.header("SETUP");
|
|
27
|
+
screen.fail("No cms.config.js found — run gogitcms-editor init first.");
|
|
28
|
+
return 1;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Started in local mode; held so the teardown below can stop it.
|
|
32
|
+
let localHandle = null;
|
|
33
|
+
let apiUrl;
|
|
34
|
+
let creds = null;
|
|
35
|
+
let brokerPlugin;
|
|
36
|
+
let runtimeConfig;
|
|
37
|
+
|
|
38
|
+
if (local) {
|
|
39
|
+
screen.setDone(0, true);
|
|
40
|
+
screen.setDone(1);
|
|
41
|
+
screen.header("DEV");
|
|
42
|
+
await screen.stream([{ text: "▲ starting the local content server…", cls: "dim" }]);
|
|
43
|
+
try {
|
|
44
|
+
localHandle = await localServer.start({
|
|
45
|
+
root: path.resolve(opts.root || process.cwd()),
|
|
46
|
+
port: opts.localPort,
|
|
47
|
+
// The server's own warnings (an unusable config, an unwatchable tree) are
|
|
48
|
+
// the user's to see; they arrive on stderr as it runs.
|
|
49
|
+
onLog: (line) => screen.line(line, "dim"),
|
|
50
|
+
});
|
|
51
|
+
} catch (e) {
|
|
52
|
+
screen.fail(e.message);
|
|
53
|
+
return 1;
|
|
54
|
+
}
|
|
55
|
+
const hs = localHandle.handshake;
|
|
56
|
+
apiUrl = hs.apiUrl;
|
|
57
|
+
// These override cms.config.mjs: local ids are derived from the working copy
|
|
58
|
+
// at startup, so a stored id from a hosted setup would address nothing.
|
|
59
|
+
runtimeConfig = {
|
|
60
|
+
...cfg,
|
|
61
|
+
API_URL: hs.apiUrl,
|
|
62
|
+
WORKSPACE_ID: hs.workspaceId,
|
|
63
|
+
REPOSITORY_ID: hs.repositoryId,
|
|
64
|
+
TOKEN_ENDPOINT: TOKEN_PATH,
|
|
65
|
+
// Tells the SPA to hide the surfaces local mode cannot serve (see
|
|
66
|
+
// config.ts). Build-time only, so a page cannot talk itself into it.
|
|
67
|
+
MODE: "local",
|
|
68
|
+
};
|
|
69
|
+
brokerPlugin = localTokenBroker(hs.token);
|
|
70
|
+
} else {
|
|
71
|
+
apiUrl = credentials.normalizeApiUrl(opts.apiUrl || cfg.API_URL);
|
|
72
|
+
creds = await credentials.load(apiUrl);
|
|
73
|
+
|
|
74
|
+
screen.setDone(0, Boolean(creds));
|
|
75
|
+
screen.setDone(1);
|
|
76
|
+
screen.header("DEV");
|
|
77
|
+
|
|
78
|
+
if (!creds) {
|
|
79
|
+
screen.fail(`Not signed in to ${apiUrl} — run gogitcms-editor login first.`);
|
|
80
|
+
return 1;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Fail before starting a server the SPA could never authenticate against.
|
|
84
|
+
try {
|
|
85
|
+
await api.accessToken(apiUrl);
|
|
86
|
+
} catch (e) {
|
|
87
|
+
screen.fail(e.message);
|
|
88
|
+
return 1;
|
|
89
|
+
}
|
|
90
|
+
runtimeConfig = { ...cfg, API_URL: apiUrl, TOKEN_ENDPOINT: TOKEN_PATH };
|
|
91
|
+
brokerPlugin = tokenBroker(apiUrl);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const appRoot = resolveAppRoot();
|
|
95
|
+
const port = Number(opts.port || 5173);
|
|
96
|
+
const host = opts.host || "localhost";
|
|
97
|
+
// Serve from the same mount the deployment uses, so a subpath install is
|
|
98
|
+
// exercised in development rather than discovered after it ships.
|
|
99
|
+
const base = config.basePathFor(cfg, opts.base);
|
|
100
|
+
|
|
101
|
+
screen.setStatus({
|
|
102
|
+
mode: "RUN",
|
|
103
|
+
auth: local ? "local files" : creds.email || "signed in",
|
|
104
|
+
authed: true,
|
|
105
|
+
hint: "ctrl-c to stop",
|
|
106
|
+
});
|
|
107
|
+
await screen.stream([
|
|
108
|
+
{ text: "▲ starting dev server…", cls: "dim" },
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
// Resolve any plugins listed in cms.config from the project directory, so the
|
|
112
|
+
// editor bundles them into the SPA the same way `cms-frontend` does.
|
|
113
|
+
let resolvedPlugins;
|
|
114
|
+
try {
|
|
115
|
+
resolvedPlugins = resolvePlugins(cfg);
|
|
116
|
+
} catch (e) {
|
|
117
|
+
screen.fail(e.message);
|
|
118
|
+
return 1;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const started = Date.now();
|
|
122
|
+
const { createServer } = await import("vite");
|
|
123
|
+
let server;
|
|
124
|
+
try {
|
|
125
|
+
server = await createServer({
|
|
126
|
+
root: appRoot,
|
|
127
|
+
base,
|
|
128
|
+
configFile: viteConfigFile(appRoot),
|
|
129
|
+
// The SPA reads these at module scope; injecting them here is what points
|
|
130
|
+
// the local editor at the real API rather than its own origin.
|
|
131
|
+
define: config.defineFor(runtimeConfig),
|
|
132
|
+
// dedupe keeps one React / design-system across the app and its plugins;
|
|
133
|
+
// the alias is how a plugin globs the project's own components directory.
|
|
134
|
+
resolve: { dedupe: PLUGIN_PEER_DEPS, alias: projectAliases() },
|
|
135
|
+
server: {
|
|
136
|
+
port,
|
|
137
|
+
strictPort: true,
|
|
138
|
+
host,
|
|
139
|
+
// Plugins live in the project's node_modules, outside the editor app
|
|
140
|
+
// root; allow the dev server to read them (and the project itself).
|
|
141
|
+
fs: { allow: [appRoot, process.cwd(), ...pluginDirs(resolvedPlugins)] },
|
|
142
|
+
},
|
|
143
|
+
plugins: [brokerPlugin, cmsPluginsVite(resolvedPlugins)],
|
|
144
|
+
});
|
|
145
|
+
await server.listen();
|
|
146
|
+
} catch (e) {
|
|
147
|
+
screen.fail(`Could not start the dev server — ${e.message}`);
|
|
148
|
+
if (localHandle) await localHandle.stop();
|
|
149
|
+
return 1;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// The app lives under `base`, so that — not the bare origin — is the URL to
|
|
153
|
+
// open; Vite only serves the SPA beneath it.
|
|
154
|
+
const url = `http://${host === "0.0.0.0" ? "localhost" : host}:${port}${base}`;
|
|
155
|
+
screen.line(` ➜ Local ${url}`, "out");
|
|
156
|
+
screen.line(` ➜ API ${apiUrl}${local ? " (local files)" : ""}`, "out");
|
|
157
|
+
if (local) screen.line(` ➜ Content ${localHandle.handshake.root}`, "out");
|
|
158
|
+
screen.line(` ➜ Editor ${path.basename(appRoot)}`, "dim");
|
|
159
|
+
screen.ok(`ready in ${Date.now() - started} ms`);
|
|
160
|
+
screen.line("");
|
|
161
|
+
if (local) {
|
|
162
|
+
// Say plainly what local mode cannot do, rather than letting the user find
|
|
163
|
+
// out by clicking something that is missing.
|
|
164
|
+
screen.line("Editing files directly. Branch switching, merges and collaboration are off.", "dim");
|
|
165
|
+
} else {
|
|
166
|
+
screen.line("Server running · run gogitcms-editor build when ready to ship.", "dim");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (opts.open) openUrl(url);
|
|
170
|
+
|
|
171
|
+
// Hold the process open until interrupted, then shut the server down cleanly
|
|
172
|
+
// so the port is free for the next run.
|
|
173
|
+
// onInterrupt overrides the CLI's default Ctrl-C teardown (see bin) so the
|
|
174
|
+
// server gets closed rather than the process shot: returning from here lets
|
|
175
|
+
// main() finish normally and restore the terminal on its way out.
|
|
176
|
+
await new Promise((resolve) => {
|
|
177
|
+
screen.onInterrupt = async () => {
|
|
178
|
+
screen.line("");
|
|
179
|
+
screen.line("Stopping dev server…", "dim");
|
|
180
|
+
await server.close().catch(() => {});
|
|
181
|
+
if (localHandle) await localHandle.stop().catch(() => {});
|
|
182
|
+
resolve();
|
|
183
|
+
};
|
|
184
|
+
});
|
|
185
|
+
return 0;
|
|
186
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// `gogitcms-editor init` — write the project's cms.config.js (step 02 of the design).
|
|
2
|
+
//
|
|
3
|
+
// The workspace and repository are picked from what the signed-in user actually
|
|
4
|
+
// has access to, so the config can't name something the editor will later fail
|
|
5
|
+
// to load. Flags cover the non-interactive path (CI, or a terminal-less shell).
|
|
6
|
+
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import * as api from "../lib/api.mjs";
|
|
9
|
+
import * as config from "../lib/config.mjs";
|
|
10
|
+
import * as credentials from "../lib/credentials.mjs";
|
|
11
|
+
import { REPOSITORIES, WORKSPACES } from "../lib/graphql.mjs";
|
|
12
|
+
import { confirm, select } from "../tui/prompts.mjs";
|
|
13
|
+
import { isInteractive } from "../tui/screen.mjs";
|
|
14
|
+
import { DEFAULT_API_URL } from "../lib/defaults.mjs";
|
|
15
|
+
|
|
16
|
+
export async function init(screen, opts) {
|
|
17
|
+
// An existing config's API_URL is the best default: re-running init in a
|
|
18
|
+
// configured project shouldn't silently retarget it at localhost.
|
|
19
|
+
const existing = await config.load();
|
|
20
|
+
const apiUrl = credentials.normalizeApiUrl(opts.apiUrl || existing?.API_URL || DEFAULT_API_URL);
|
|
21
|
+
|
|
22
|
+
screen.header("SETUP");
|
|
23
|
+
const creds = await credentials.load(apiUrl);
|
|
24
|
+
if (!creds) {
|
|
25
|
+
screen.fail(`Not signed in to ${apiUrl} — run gogitcms-editor login first.`);
|
|
26
|
+
return 1;
|
|
27
|
+
}
|
|
28
|
+
screen.setDone(0);
|
|
29
|
+
screen.setStatus({ auth: creds.email || "signed in", authed: true, hint: "next: gogitcms-editor dev" });
|
|
30
|
+
|
|
31
|
+
const current = config.find();
|
|
32
|
+
if (current && !opts.yes) {
|
|
33
|
+
const overwrite = await confirm(
|
|
34
|
+
screen,
|
|
35
|
+
`${path.basename(current)} already exists. Overwrite it?`,
|
|
36
|
+
false,
|
|
37
|
+
);
|
|
38
|
+
if (!overwrite) {
|
|
39
|
+
screen.line("Left the existing config alone.", "dim");
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
await screen.stream([{ text: "Loading your workspaces…", cls: "dim" }]);
|
|
45
|
+
|
|
46
|
+
let workspaces;
|
|
47
|
+
try {
|
|
48
|
+
({ workspaces } = await api.graphql(apiUrl, WORKSPACES));
|
|
49
|
+
} catch (e) {
|
|
50
|
+
screen.fail(e.message);
|
|
51
|
+
return 1;
|
|
52
|
+
}
|
|
53
|
+
if (!workspaces?.length) {
|
|
54
|
+
screen.fail("You have no workspaces yet. Create one in the dashboard first.");
|
|
55
|
+
return 1;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const workspace = await pick(screen, {
|
|
59
|
+
what: "workspace",
|
|
60
|
+
flag: "--workspace",
|
|
61
|
+
items: workspaces,
|
|
62
|
+
label: (w) => `${w.name} (${w.slug})`,
|
|
63
|
+
match: (w, v) => w.id === v || w.slug === v || w.name === v,
|
|
64
|
+
requested: opts.workspace,
|
|
65
|
+
});
|
|
66
|
+
if (!workspace) return 1;
|
|
67
|
+
|
|
68
|
+
const { repositories } = await api.graphql(apiUrl, REPOSITORIES, { workspaceId: workspace.id });
|
|
69
|
+
if (!repositories?.length) {
|
|
70
|
+
screen.fail(`No repositories connected to ${workspace.name}. Connect one in the dashboard first.`);
|
|
71
|
+
return 1;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const repository = await pick(screen, {
|
|
75
|
+
what: "repository",
|
|
76
|
+
flag: "--repository",
|
|
77
|
+
items: repositories,
|
|
78
|
+
label: (r) => `${r.owner}/${r.name}`,
|
|
79
|
+
match: (r, v) => r.id === v || `${r.owner}/${r.name}` === v || r.name === v,
|
|
80
|
+
requested: opts.repository,
|
|
81
|
+
});
|
|
82
|
+
if (!repository) return 1;
|
|
83
|
+
|
|
84
|
+
const written = {
|
|
85
|
+
API_URL: apiUrl,
|
|
86
|
+
WORKSPACE_ID: workspace.id,
|
|
87
|
+
REPOSITORY_ID: repository.id,
|
|
88
|
+
// Carried over when re-running init, so a subpath deployment isn't silently
|
|
89
|
+
// reset to the root.
|
|
90
|
+
BASE_PATH: opts.base || existing?.BASE_PATH || "/",
|
|
91
|
+
};
|
|
92
|
+
const file = await config.write(written);
|
|
93
|
+
|
|
94
|
+
// Echo the file back the way the design renders it: a header line, then the
|
|
95
|
+
// body, so the user sees exactly what landed in their project.
|
|
96
|
+
screen.line("");
|
|
97
|
+
screen.line(path.basename(file), "filehdr");
|
|
98
|
+
for (const l of config.render(written).trimEnd().split("\n")) {
|
|
99
|
+
screen.line(` ${l}`, "fileline");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
screen.setDone(1);
|
|
103
|
+
await screen.stream([
|
|
104
|
+
"",
|
|
105
|
+
{ text: `✓ Wrote ${path.basename(file)}`, cls: "ok" },
|
|
106
|
+
{ text: ` ${workspace.name} · ${repository.owner}/${repository.name}`, cls: "dim" },
|
|
107
|
+
"",
|
|
108
|
+
{ text: "Next — run gogitcms-editor dev to start the local editor.", cls: "dim" },
|
|
109
|
+
]);
|
|
110
|
+
return 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// pick resolves one choice: from a flag when given, from a menu when the
|
|
114
|
+
// terminal allows it, and automatically when there is only one candidate.
|
|
115
|
+
async function pick(screen, { what, flag, items, label, match, requested }) {
|
|
116
|
+
if (requested) {
|
|
117
|
+
const found = items.find((i) => match(i, requested));
|
|
118
|
+
if (!found) {
|
|
119
|
+
screen.fail(`No ${what} matching ${JSON.stringify(requested)}. Available: ${items.map(label).join(", ")}`);
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
screen.line(`${what}: ${label(found)}`, "dim");
|
|
123
|
+
return found;
|
|
124
|
+
}
|
|
125
|
+
if (items.length === 1) {
|
|
126
|
+
screen.line(`${what}: ${label(items[0])}`, "dim");
|
|
127
|
+
return items[0];
|
|
128
|
+
}
|
|
129
|
+
if (!isInteractive()) {
|
|
130
|
+
screen.fail(`Several ${what}s available — pass ${flag}. Options: ${items.map(label).join(", ")}`);
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
const chosen = await select(
|
|
134
|
+
screen,
|
|
135
|
+
`Select a ${what}`,
|
|
136
|
+
items.map((i) => ({ label: label(i), value: i })),
|
|
137
|
+
);
|
|
138
|
+
if (!chosen) {
|
|
139
|
+
screen.line("Cancelled.", "dim");
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
screen.line(`${what}: ${label(chosen)}`, "dim");
|
|
143
|
+
return chosen;
|
|
144
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// `gogitcms-editor login` — the device grant (step 01 of the design).
|
|
2
|
+
//
|
|
3
|
+
// The CLI asks the server for a code pair, shows the short one and the page to
|
|
4
|
+
// enter it on, then polls until a signed-in browser approves. Nothing is typed
|
|
5
|
+
// into the terminal: the password never touches this process.
|
|
6
|
+
|
|
7
|
+
import * as api from "../lib/api.mjs";
|
|
8
|
+
import * as credentials from "../lib/credentials.mjs";
|
|
9
|
+
import { openUrl } from "../lib/open.mjs";
|
|
10
|
+
import { confirm } from "../tui/prompts.mjs";
|
|
11
|
+
import { DEFAULT_API_URL } from "../lib/defaults.mjs";
|
|
12
|
+
|
|
13
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
14
|
+
|
|
15
|
+
export async function login(screen, opts) {
|
|
16
|
+
const apiUrl = credentials.normalizeApiUrl(opts.apiUrl || DEFAULT_API_URL);
|
|
17
|
+
screen.header("SETUP");
|
|
18
|
+
screen.setStatus({ mode: "AUTH", hint: "waiting for approval" });
|
|
19
|
+
|
|
20
|
+
await screen.stream([{ text: `Requesting device authorization from ${apiUrl}…`, cls: "dim" }, ""]);
|
|
21
|
+
|
|
22
|
+
let pair;
|
|
23
|
+
try {
|
|
24
|
+
pair = await api.requestDeviceCode(apiUrl);
|
|
25
|
+
} catch (e) {
|
|
26
|
+
screen.fail(`Could not reach ${apiUrl} — ${e.message}`);
|
|
27
|
+
return 1;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
screen.line("FIRST — copy your one-time code", "label");
|
|
31
|
+
screen.line(pair.userCode, "code");
|
|
32
|
+
screen.line("");
|
|
33
|
+
screen.line("THEN — open the link and enter it", "label");
|
|
34
|
+
screen.line(`→ ${pair.verificationUri}`, "out");
|
|
35
|
+
screen.line("");
|
|
36
|
+
|
|
37
|
+
if (await confirm(screen, "Open that page in your browser now?")) {
|
|
38
|
+
// verificationUriComplete carries the code, so the page can resolve it
|
|
39
|
+
// without the user retyping what they just read.
|
|
40
|
+
openUrl(pair.verificationUriComplete || pair.verificationUri);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const stop = screen.startSpinner("Waiting for approval…");
|
|
44
|
+
const result = await pollForToken(apiUrl, pair);
|
|
45
|
+
stop();
|
|
46
|
+
|
|
47
|
+
if (!result.ok) {
|
|
48
|
+
screen.fail(result.message);
|
|
49
|
+
screen.setStatus({ mode: "READY", hint: "next: gogitcms-editor login" });
|
|
50
|
+
return 1;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Identify the session so the status line can show who is signed in. A failure
|
|
54
|
+
// here is cosmetic — the tokens are already valid.
|
|
55
|
+
let email = "";
|
|
56
|
+
try {
|
|
57
|
+
email = (await api.me(apiUrl, result.tokens.accessToken)).email;
|
|
58
|
+
} catch {
|
|
59
|
+
email = "signed in";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
await credentials.save(apiUrl, {
|
|
63
|
+
accessToken: result.tokens.accessToken,
|
|
64
|
+
refreshToken: result.tokens.refreshToken,
|
|
65
|
+
expiresAt: credentials.expiresAtFrom(result.tokens.accessToken),
|
|
66
|
+
email,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
screen.setDone(0);
|
|
70
|
+
screen.setStatus({ mode: "READY", auth: email, authed: true, hint: "next: gogitcms-editor init" });
|
|
71
|
+
await screen.stream([
|
|
72
|
+
{ text: "✓ Authorized in browser", cls: "ok" },
|
|
73
|
+
{ text: `✓ Signed in as ${email}`, cls: "ok" },
|
|
74
|
+
{ text: ` credentials → ${credentials.credentialsPath}`, cls: "dim" },
|
|
75
|
+
"",
|
|
76
|
+
{ text: "Next — run gogitcms-editor init to create your config.", cls: "dim" },
|
|
77
|
+
]);
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// pollForToken asks for the session on the server's cadence until it is
|
|
82
|
+
// approved, rejected, or the code expires.
|
|
83
|
+
async function pollForToken(apiUrl, pair) {
|
|
84
|
+
const deadline = Date.now() + (pair.expiresIn || 900) * 1000;
|
|
85
|
+
let interval = (pair.interval || 5) * 1000;
|
|
86
|
+
|
|
87
|
+
while (Date.now() < deadline) {
|
|
88
|
+
await sleep(interval);
|
|
89
|
+
try {
|
|
90
|
+
const tokens = await api.pollDeviceToken(apiUrl, pair.deviceCode);
|
|
91
|
+
return { ok: true, tokens };
|
|
92
|
+
} catch (e) {
|
|
93
|
+
switch (e.slug) {
|
|
94
|
+
case "authorization_pending":
|
|
95
|
+
continue;
|
|
96
|
+
case "slow_down":
|
|
97
|
+
// The server is asking for more room; back off rather than keep the
|
|
98
|
+
// same cadence and risk being cut off.
|
|
99
|
+
interval += 5000;
|
|
100
|
+
continue;
|
|
101
|
+
case "access_denied":
|
|
102
|
+
return { ok: false, message: "Request rejected in the browser. Nothing was signed in." };
|
|
103
|
+
case "expired_token":
|
|
104
|
+
return { ok: false, message: "That code expired. Run gogitcms-editor login again." };
|
|
105
|
+
case "invalid_grant":
|
|
106
|
+
return { ok: false, message: "That code is no longer valid. Run gogitcms-editor login again." };
|
|
107
|
+
default:
|
|
108
|
+
// A transient network blip shouldn't abandon a flow the user has
|
|
109
|
+
// already approved in their browser — keep polling until the deadline.
|
|
110
|
+
if (!e.status) continue;
|
|
111
|
+
return { ok: false, message: e.message };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return { ok: false, message: "Timed out waiting for approval. Run gogitcms-editor login again." };
|
|
116
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// `gogitcms-editor status` — where the three setup steps stand, and `logout`.
|
|
2
|
+
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import * as config from "../lib/config.mjs";
|
|
5
|
+
import * as credentials from "../lib/credentials.mjs";
|
|
6
|
+
import { STEPS } from "../tui/theme.mjs";
|
|
7
|
+
import { DEFAULT_API_URL } from "../lib/defaults.mjs";
|
|
8
|
+
|
|
9
|
+
// resolveSession finds which server this project belongs to and whether we hold
|
|
10
|
+
// a session for it: the config's API_URL when there is one, otherwise the single
|
|
11
|
+
// stored session, otherwise the default.
|
|
12
|
+
export async function resolveSession(opts = {}) {
|
|
13
|
+
const cfg = await config.load();
|
|
14
|
+
const apiUrl = credentials.normalizeApiUrl(opts.apiUrl || cfg?.API_URL || "");
|
|
15
|
+
if (apiUrl) return { apiUrl, cfg, creds: await credentials.load(apiUrl) };
|
|
16
|
+
|
|
17
|
+
const any = await credentials.loadAny();
|
|
18
|
+
if (any) return { apiUrl: any[0], cfg, creds: any[1] };
|
|
19
|
+
return { apiUrl: credentials.normalizeApiUrl(DEFAULT_API_URL), cfg, creds: null };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function status(screen, opts) {
|
|
23
|
+
const { apiUrl, cfg, creds } = await resolveSession(opts);
|
|
24
|
+
const configFile = config.find();
|
|
25
|
+
const done = [Boolean(creds), Boolean(cfg), false];
|
|
26
|
+
done.forEach((d, i) => screen.setDone(i, d));
|
|
27
|
+
|
|
28
|
+
screen.setStatus({
|
|
29
|
+
mode: done[0] && done[1] ? "READY" : "READY",
|
|
30
|
+
auth: creds?.email || "not logged in",
|
|
31
|
+
authed: Boolean(creds),
|
|
32
|
+
hint: nextHint(done),
|
|
33
|
+
});
|
|
34
|
+
screen.header("STATUS");
|
|
35
|
+
|
|
36
|
+
screen.line("setup progress", "label");
|
|
37
|
+
STEPS.forEach((s, i) => {
|
|
38
|
+
screen.line(` [${done[i] ? "x" : " "}] ${s.label} · ${s.cmd}`, done[i] ? "ok" : "out");
|
|
39
|
+
});
|
|
40
|
+
screen.line("");
|
|
41
|
+
screen.line(` server ${apiUrl}`, "dim");
|
|
42
|
+
screen.line(` account ${creds?.email || "—"}`, "dim");
|
|
43
|
+
screen.line(` config ${configFile ? path.relative(process.cwd(), configFile) : "—"}`, "dim");
|
|
44
|
+
if (cfg?.REPOSITORY_ID) screen.line(` repository ${cfg.REPOSITORY_ID}`, "dim");
|
|
45
|
+
screen.line("");
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function nextHint(done) {
|
|
50
|
+
const next = done.findIndex((d) => !d);
|
|
51
|
+
return next === -1 ? "gogitcms-editor build → deploy" : `next: ${STEPS[next].cmd}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function logout(screen, opts) {
|
|
55
|
+
const { apiUrl, creds } = await resolveSession(opts);
|
|
56
|
+
screen.header("SETUP");
|
|
57
|
+
if (!creds) {
|
|
58
|
+
screen.line(`No stored session for ${apiUrl}.`, "dim");
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
await credentials.clear(apiUrl);
|
|
62
|
+
screen.setStatus({ auth: "not logged in", authed: false, hint: "next: gogitcms-editor login" });
|
|
63
|
+
screen.ok(`Signed out of ${apiUrl}`);
|
|
64
|
+
// Only this machine's copy is dropped; the session itself is revoked from the
|
|
65
|
+
// dashboard, so say so rather than implying a global sign-out.
|
|
66
|
+
screen.line(" The session can also be revoked in the dashboard under Account → Security.", "dim");
|
|
67
|
+
return 0;
|
|
68
|
+
}
|