studiograph 1.3.3-next.9 → 1.3.4
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/agent/orchestrator.d.ts +2 -0
- package/dist/agent/orchestrator.js +13 -5
- package/dist/agent/orchestrator.js.map +1 -1
- package/dist/agent/skills/sync-configuration.md +4 -29
- package/dist/agent/skills/sync-setup.md +2 -4
- package/dist/agent/tools/ops-tools.js +15 -126
- package/dist/agent/tools/ops-tools.js.map +1 -1
- package/dist/agent/tools/sync-tools.d.ts +7 -6
- package/dist/agent/tools/sync-tools.js +205 -178
- package/dist/agent/tools/sync-tools.js.map +1 -1
- package/dist/cli/commands/about.d.ts +13 -0
- package/dist/cli/commands/about.js +97 -0
- package/dist/cli/commands/about.js.map +1 -0
- package/dist/cli/commands/clone.d.ts +5 -2
- package/dist/cli/commands/clone.js +131 -62
- package/dist/cli/commands/clone.js.map +1 -1
- package/dist/cli/commands/connector.d.ts +2 -16
- package/dist/cli/commands/connector.js +32 -109
- package/dist/cli/commands/connector.js.map +1 -1
- package/dist/cli/commands/deploy.d.ts +0 -1
- package/dist/cli/commands/deploy.js +13 -103
- package/dist/cli/commands/deploy.js.map +1 -1
- package/dist/cli/commands/init.js +6 -93
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/join.js +51 -1
- package/dist/cli/commands/join.js.map +1 -1
- package/dist/cli/commands/redeploy.js +1 -2
- package/dist/cli/commands/redeploy.js.map +1 -1
- package/dist/cli/commands/serve.d.ts +1 -3
- package/dist/cli/commands/serve.js +29 -109
- package/dist/cli/commands/serve.js.map +1 -1
- package/dist/cli/commands/start.js +1 -1
- package/dist/cli/commands/start.js.map +1 -1
- package/dist/cli/commands/sync-collection.d.ts +14 -0
- package/dist/cli/commands/sync-collection.js +366 -0
- package/dist/cli/commands/sync-collection.js.map +1 -0
- package/dist/cli/commands/sync.js +518 -82
- package/dist/cli/commands/sync.js.map +1 -1
- package/dist/cli/index.js +12 -30
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/setup-wizard.d.ts +0 -13
- package/dist/cli/setup-wizard.js +6 -81
- package/dist/cli/setup-wizard.js.map +1 -1
- package/dist/core/types.d.ts +140 -21
- package/dist/core/types.js +15 -4
- package/dist/core/types.js.map +1 -1
- package/dist/core/workspace.d.ts +11 -4
- package/dist/core/workspace.js +61 -26
- package/dist/core/workspace.js.map +1 -1
- package/dist/integrations/asana.d.ts +26 -0
- package/dist/integrations/asana.js +77 -0
- package/dist/integrations/asana.js.map +1 -0
- package/dist/integrations/figma-local.d.ts +16 -0
- package/dist/integrations/figma-local.js +10 -0
- package/dist/integrations/figma-local.js.map +1 -0
- package/dist/integrations/figma.d.ts +17 -0
- package/dist/integrations/figma.js +16 -0
- package/dist/integrations/figma.js.map +1 -0
- package/dist/integrations/granola.d.ts +24 -0
- package/dist/integrations/granola.js +59 -0
- package/dist/integrations/granola.js.map +1 -0
- package/dist/integrations/linear.d.ts +14 -0
- package/dist/integrations/linear.js +70 -0
- package/dist/integrations/linear.js.map +1 -0
- package/dist/integrations/paper-local.d.ts +14 -0
- package/dist/integrations/paper-local.js +10 -0
- package/dist/integrations/paper-local.js.map +1 -0
- package/dist/integrations/paper.d.ts +2 -0
- package/dist/integrations/paper.js +10 -0
- package/dist/integrations/paper.js.map +1 -0
- package/dist/integrations/pipedrive.d.ts +26 -0
- package/dist/integrations/pipedrive.js +97 -0
- package/dist/integrations/pipedrive.js.map +1 -0
- package/dist/integrations/registry.d.ts +15 -0
- package/dist/integrations/registry.js +27 -0
- package/dist/integrations/registry.js.map +1 -0
- package/dist/integrations/types.d.ts +34 -0
- package/dist/integrations/types.js +9 -0
- package/dist/integrations/types.js.map +1 -0
- package/dist/mcp/connector-manager.d.ts +45 -31
- package/dist/mcp/connector-manager.js +164 -116
- package/dist/mcp/connector-manager.js.map +1 -1
- package/dist/mcp/server-oauth-provider.d.ts +56 -0
- package/dist/mcp/server-oauth-provider.js +138 -0
- package/dist/mcp/server-oauth-provider.js.map +1 -0
- package/dist/server/chrome/chrome.css +142 -28
- package/dist/server/chrome/chrome.js +51 -25
- package/dist/server/collab-authority.d.ts +70 -0
- package/dist/server/collab-authority.js +218 -0
- package/dist/server/collab-authority.js.map +1 -0
- package/dist/server/collab.d.ts +29 -0
- package/dist/server/collab.js +195 -0
- package/dist/server/collab.js.map +1 -0
- package/dist/server/commit-scheduler.d.ts +1 -1
- package/dist/server/commit-scheduler.js +18 -4
- package/dist/server/commit-scheduler.js.map +1 -1
- package/dist/server/index.d.ts +0 -2
- package/dist/server/index.js +89 -18
- package/dist/server/index.js.map +1 -1
- package/dist/server/routes/auth-api.d.ts +6 -0
- package/dist/server/routes/auth-api.js +55 -0
- package/dist/server/routes/auth-api.js.map +1 -1
- package/dist/server/routes/collab.d.ts +6 -0
- package/dist/server/routes/collab.js +10 -0
- package/dist/server/routes/collab.js.map +1 -0
- package/dist/server/routes/git-http.d.ts +1 -1
- package/dist/server/routes/git-http.js +53 -15
- package/dist/server/routes/git-http.js.map +1 -1
- package/dist/server/routes/graph-api.d.ts +2 -2
- package/dist/server/routes/graph-api.js +59 -55
- package/dist/server/routes/graph-api.js.map +1 -1
- package/dist/server/routes/mcp.d.ts +12 -0
- package/dist/server/routes/mcp.js +35 -0
- package/dist/server/routes/mcp.js.map +1 -0
- package/dist/server/routes/permissions-api.d.ts +2 -1
- package/dist/server/routes/permissions-api.js +16 -2
- package/dist/server/routes/permissions-api.js.map +1 -1
- package/dist/server/routes/sync-api.d.ts +26 -0
- package/dist/server/routes/sync-api.js +757 -0
- package/dist/server/routes/sync-api.js.map +1 -0
- package/dist/server/routes/ws.d.ts +4 -2
- package/dist/server/routes/ws.js +100 -4
- package/dist/server/routes/ws.js.map +1 -1
- package/dist/server/session-manager.d.ts +40 -0
- package/dist/server/session-manager.js +132 -0
- package/dist/server/session-manager.js.map +1 -0
- package/dist/server/ws-hub.d.ts +95 -1
- package/dist/server/ws-hub.js +192 -5
- package/dist/server/ws-hub.js.map +1 -1
- package/dist/server/yjs-manager.d.ts +59 -0
- package/dist/server/yjs-manager.js +194 -0
- package/dist/server/yjs-manager.js.map +1 -0
- package/dist/services/auth-service.d.ts +30 -2
- package/dist/services/auth-service.js +116 -11
- package/dist/services/auth-service.js.map +1 -1
- package/dist/services/git.d.ts +6 -0
- package/dist/services/git.js +32 -2
- package/dist/services/git.js.map +1 -1
- package/dist/services/sync/collection-sync.d.ts +73 -0
- package/dist/services/sync/collection-sync.js +726 -0
- package/dist/services/sync/collection-sync.js.map +1 -0
- package/dist/services/sync/commit.js +5 -20
- package/dist/services/sync/commit.js.map +1 -1
- package/dist/services/sync/data-fetcher.d.ts +31 -0
- package/dist/services/sync/data-fetcher.js +12 -0
- package/dist/services/sync/data-fetcher.js.map +1 -0
- package/dist/services/sync/entity-refresh.d.ts +30 -0
- package/dist/services/sync/entity-refresh.js +275 -0
- package/dist/services/sync/entity-refresh.js.map +1 -0
- package/dist/services/sync/frontmatter-extractor.d.ts +2 -2
- package/dist/services/sync/frontmatter-extractor.js +1 -2
- package/dist/services/sync/frontmatter-extractor.js.map +1 -1
- package/dist/services/sync/graph-match.js +1 -1
- package/dist/services/sync/graph-match.js.map +1 -1
- package/dist/services/sync/mcp-client.d.ts +16 -4
- package/dist/services/sync/mcp-client.js +34 -20
- package/dist/services/sync/mcp-client.js.map +1 -1
- package/dist/services/sync/prompts.js +1 -1
- package/dist/services/sync/reconciler.js +1 -2
- package/dist/services/sync/reconciler.js.map +1 -1
- package/dist/services/sync/rest-client.d.ts +40 -0
- package/dist/services/sync/rest-client.js +100 -0
- package/dist/services/sync/rest-client.js.map +1 -0
- package/dist/services/sync/source-config.d.ts +23 -1
- package/dist/services/sync/source-config.js +112 -16
- package/dist/services/sync/source-config.js.map +1 -1
- package/dist/services/sync/source-definitions/asana.d.ts +3 -4
- package/dist/services/sync/source-definitions/asana.js +7 -13
- package/dist/services/sync/source-definitions/asana.js.map +1 -1
- package/dist/services/sync/source-definitions/definitions.d.ts +5 -18
- package/dist/services/sync/source-definitions/definitions.js +4 -22
- package/dist/services/sync/source-definitions/definitions.js.map +1 -1
- package/dist/services/sync/source-definitions/granola.d.ts +1 -1
- package/dist/services/sync/source-definitions/granola.js +11 -18
- package/dist/services/sync/source-definitions/granola.js.map +1 -1
- package/dist/services/sync/source-definitions/linear.d.ts +1 -1
- package/dist/services/sync/source-definitions/linear.js +17 -15
- package/dist/services/sync/source-definitions/linear.js.map +1 -1
- package/dist/services/sync/source-definitions/pipedrive.d.ts +1 -1
- package/dist/services/sync/source-definitions/pipedrive.js +6 -15
- package/dist/services/sync/source-definitions/pipedrive.js.map +1 -1
- package/dist/services/sync/staging.js +1 -2
- package/dist/services/sync/staging.js.map +1 -1
- package/dist/services/sync/structured-extractor.d.ts +8 -2
- package/dist/services/sync/structured-extractor.js +243 -35
- package/dist/services/sync/structured-extractor.js.map +1 -1
- package/dist/services/sync/types.d.ts +192 -23
- package/dist/services/sync/unstructured-extractor.d.ts +1 -3
- package/dist/services/sync/unstructured-extractor.js +2 -14
- package/dist/services/sync/unstructured-extractor.js.map +1 -1
- package/dist/utils/git.d.ts +24 -20
- package/dist/utils/git.js +99 -65
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/preflight.d.ts +1 -15
- package/dist/utils/preflight.js +1 -35
- package/dist/utils/preflight.js.map +1 -1
- package/dist/web/_app/immutable/assets/0.CupILLQs.css +1 -0
- package/dist/web/_app/immutable/assets/3.CtJi4Cy9.css +1 -0
- package/dist/web/_app/immutable/assets/5.CydFyZSu.css +1 -0
- package/dist/web/_app/immutable/assets/6.kqeOo0OW.css +1 -0
- package/dist/web/_app/immutable/assets/7.CseIx7qQ.css +1 -0
- package/dist/web/_app/immutable/assets/{8.Sm6jB3a0.css → 8.BYpFDZHK.css} +1 -1
- package/dist/web/_app/immutable/assets/AppShell.Ch_ef9hJ.css +1 -0
- package/dist/web/_app/immutable/assets/ChatPanel.CP-_8txt.css +1 -0
- package/dist/web/_app/immutable/chunks/0oxpWEgM.js +1 -0
- package/dist/web/_app/immutable/chunks/B1y7Wy5O.js +18 -0
- package/dist/web/_app/immutable/chunks/B7eduG_j.js +64 -0
- package/dist/web/_app/immutable/chunks/BBLgaWN8.js +1 -0
- package/dist/web/_app/immutable/chunks/BCB5cYCz.js +2 -0
- package/dist/web/_app/immutable/chunks/{aosHekRC.js → BPUy9_sS.js} +1 -1
- package/dist/web/_app/immutable/chunks/BVBRzmeQ.js +7 -0
- package/dist/web/_app/immutable/chunks/{CUzqHQY_.js → BXuvR8Ks.js} +2 -1
- package/dist/web/_app/immutable/chunks/BeBar3OL.js +1 -0
- package/dist/web/_app/immutable/chunks/BuOTIbJu.js +1 -0
- package/dist/web/_app/immutable/chunks/CLFba8FK.js +5 -0
- package/dist/web/_app/immutable/chunks/CQCkXCml.js +1 -0
- package/dist/web/_app/immutable/chunks/CXuhHL4d.js +1 -0
- package/dist/web/_app/immutable/chunks/Cg9NOuOl.js +27 -0
- package/dist/web/_app/immutable/chunks/Cs5oz2oJ.js +5 -0
- package/dist/web/_app/immutable/chunks/Cs_ROD7H.js +2 -0
- package/dist/web/_app/immutable/chunks/D2aTbzFm.js +3 -0
- package/dist/web/_app/immutable/chunks/D4FXhiC2.js +1 -0
- package/dist/web/_app/immutable/chunks/D4VHRYeB.js +1 -0
- package/dist/web/_app/immutable/chunks/DCGSm8Hl.js +1 -0
- package/dist/web/_app/immutable/chunks/DP09rP34.js +2 -0
- package/dist/web/_app/immutable/chunks/DiP47fAp.js +1 -0
- package/dist/web/_app/immutable/chunks/DptGlK8O.js +1 -0
- package/dist/web/_app/immutable/chunks/O0fx2ss6.js +1 -0
- package/dist/web/_app/immutable/chunks/xBRYfpah.js +1 -0
- package/dist/web/_app/immutable/entry/app.FgnywZP_.js +2 -0
- package/dist/web/_app/immutable/entry/start.Bsa-zlPf.js +1 -0
- package/dist/web/_app/immutable/nodes/0.D3SW-LMc.js +10 -0
- package/dist/web/_app/immutable/nodes/1.y0c5TQTP.js +1 -0
- package/dist/web/_app/immutable/nodes/2.BQfSep9-.js +1 -0
- package/dist/web/_app/immutable/nodes/3.CC4Y-xMM.js +11 -0
- package/dist/web/_app/immutable/nodes/4.Dp0Z-oPW.js +4 -0
- package/dist/web/_app/immutable/nodes/5.gjZ03DON.js +2 -0
- package/dist/web/_app/immutable/nodes/6.dRNIwcJQ.js +1 -0
- package/dist/web/_app/immutable/nodes/7.I4Gjes3o.js +2 -0
- package/dist/web/_app/immutable/nodes/8.Dj14D7uH.js +1 -0
- package/dist/web/_app/version.json +1 -1
- package/dist/web/index.html +10 -12
- package/package.json +4 -2
- package/dist/web/_app/immutable/assets/0.CF0XhAap.css +0 -1
- package/dist/web/_app/immutable/assets/3.BJy7pVXi.css +0 -1
- package/dist/web/_app/immutable/assets/4.Ad16uh9o.css +0 -1
- package/dist/web/_app/immutable/assets/6.Bm2i7O0j.css +0 -1
- package/dist/web/_app/immutable/assets/7.Cn2DG-J6.css +0 -1
- package/dist/web/_app/immutable/assets/AppShell.CztjTuKY.css +0 -1
- package/dist/web/_app/immutable/assets/ChatPanel.RFD5GGYI.css +0 -1
- package/dist/web/_app/immutable/assets/editor.CPAf2SRV.css +0 -1
- package/dist/web/_app/immutable/chunks/4QY4j-jX.js +0 -1
- package/dist/web/_app/immutable/chunks/B3Kdf1r4.js +0 -6
- package/dist/web/_app/immutable/chunks/BIo3H1KR.js +0 -2
- package/dist/web/_app/immutable/chunks/BJLM1w2L.js +0 -23
- package/dist/web/_app/immutable/chunks/BLCwEMdm.js +0 -1
- package/dist/web/_app/immutable/chunks/BSYvCVJt.js +0 -1
- package/dist/web/_app/immutable/chunks/Ba5JX1o9.js +0 -1
- package/dist/web/_app/immutable/chunks/Bj34y868.js +0 -64
- package/dist/web/_app/immutable/chunks/BrWpHgBJ.js +0 -1
- package/dist/web/_app/immutable/chunks/C3g_lwol.js +0 -1
- package/dist/web/_app/immutable/chunks/CXnPm09s.js +0 -1
- package/dist/web/_app/immutable/chunks/ClOTom10.js +0 -1
- package/dist/web/_app/immutable/chunks/CtT4aw_G.js +0 -1
- package/dist/web/_app/immutable/chunks/Dh_H7Owr.js +0 -18
- package/dist/web/_app/immutable/chunks/DnlgZ_Tk.js +0 -5
- package/dist/web/_app/immutable/chunks/DtVH--hH.js +0 -6
- package/dist/web/_app/immutable/chunks/Dzd9kdLj.js +0 -2
- package/dist/web/_app/immutable/chunks/L91a_BGe.js +0 -1
- package/dist/web/_app/immutable/chunks/bHAllEMt.js +0 -1
- package/dist/web/_app/immutable/entry/app.CdrgaaFb.js +0 -2
- package/dist/web/_app/immutable/entry/start.t9LMjt48.js +0 -1
- package/dist/web/_app/immutable/nodes/0.P-Xfebn4.js +0 -2
- package/dist/web/_app/immutable/nodes/1.DiZlq1e6.js +0 -1
- package/dist/web/_app/immutable/nodes/2.ByIZ5J2p.js +0 -1
- package/dist/web/_app/immutable/nodes/3.D7JhktsZ.js +0 -1
- package/dist/web/_app/immutable/nodes/4.BtMeWbPx.js +0 -16
- package/dist/web/_app/immutable/nodes/5.CCYG1pbQ.js +0 -4
- package/dist/web/_app/immutable/nodes/6.CL_Ah04j.js +0 -2
- package/dist/web/_app/immutable/nodes/7.BMTaosAj.js +0 -1
- package/dist/web/_app/immutable/nodes/8.mrPg67cz.js +0 -1
- /package/dist/web/_app/immutable/assets/{5.BhKgiXd2.css → 4.BhKgiXd2.css} +0 -0
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
* Works with both GitHub remotes and the embedded git server.
|
|
9
9
|
*/
|
|
10
10
|
import { Command } from 'commander';
|
|
11
|
-
import { log, outro } from '@clack/prompts';
|
|
12
|
-
import { existsSync } from 'fs';
|
|
11
|
+
import { log, outro, text, password as passwordPrompt } from '@clack/prompts';
|
|
12
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'fs';
|
|
13
13
|
import { join } from 'path';
|
|
14
14
|
import { Workspace } from '../../core/workspace.js';
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
15
|
+
import { AuthService } from '../../services/auth-service.js';
|
|
16
|
+
import { ConnectorManager } from '../../mcp/connector-manager.js';
|
|
17
|
+
import { gitPull, gitPush, gitPushWithCredentials, gitSetRemote, gitHasRemote } from '../../utils/git.js';
|
|
17
18
|
async function getRepoEntries(workspace, workspaceRoot) {
|
|
18
19
|
const entries = [];
|
|
19
20
|
// The .studiograph config directory is itself a git repo (when GitHub-provisioned)
|
|
@@ -36,47 +37,148 @@ function isGitRepo(path) {
|
|
|
36
37
|
return existsSync(path) && existsSync(join(path, '.git'));
|
|
37
38
|
}
|
|
38
39
|
/**
|
|
39
|
-
* Print
|
|
40
|
+
* Print manual resolution guidance when pull has diverged.
|
|
40
41
|
*/
|
|
41
|
-
function
|
|
42
|
-
|
|
42
|
+
function logManualGuidance(entryPath) {
|
|
43
|
+
log.info(` cd ${entryPath} && git pull --rebase`);
|
|
44
|
+
}
|
|
45
|
+
async function pullRemoteConfig(remoteUrl) {
|
|
46
|
+
const workspace = new Workspace();
|
|
47
|
+
if (!workspace.isWorkspace()) {
|
|
48
|
+
console.error('❌ Not a Studiograph workspace. Run `studiograph init` first.');
|
|
49
|
+
process.exit(1);
|
|
43
50
|
return;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
}
|
|
52
|
+
const base = remoteUrl.replace(/\/+$/, '');
|
|
53
|
+
// Prompt for admin credentials
|
|
54
|
+
const email = await text({ message: 'Admin email:', validate: (v) => v.includes('@') ? undefined : 'Enter a valid email' });
|
|
55
|
+
if (typeof email === 'symbol') {
|
|
56
|
+
outro('Cancelled');
|
|
57
|
+
process.exit(0);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const password = await passwordPrompt({ message: 'Password:' });
|
|
61
|
+
if (typeof password === 'symbol') {
|
|
62
|
+
outro('Cancelled');
|
|
63
|
+
process.exit(0);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
// Login to get session token
|
|
67
|
+
log.info('Authenticating...');
|
|
68
|
+
let token;
|
|
69
|
+
try {
|
|
70
|
+
const loginRes = await fetch(`${base}/api/auth/login`, {
|
|
71
|
+
method: 'POST',
|
|
72
|
+
headers: { 'Content-Type': 'application/json' },
|
|
73
|
+
body: JSON.stringify({ email, password }),
|
|
74
|
+
});
|
|
75
|
+
if (!loginRes.ok) {
|
|
76
|
+
const body = await loginRes.json().catch(() => ({}));
|
|
77
|
+
outro(`Login failed: ${body.error || loginRes.statusText}`);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const cookies = loginRes.headers.getSetCookie?.() ?? [];
|
|
82
|
+
const tokenCookie = cookies.find(c => c.startsWith('__sg_token='));
|
|
83
|
+
if (!tokenCookie) {
|
|
84
|
+
outro('Login succeeded but no session cookie was returned');
|
|
85
|
+
process.exit(1);
|
|
86
|
+
return;
|
|
51
87
|
}
|
|
88
|
+
token = tokenCookie.split(';')[0];
|
|
52
89
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
90
|
+
catch (err) {
|
|
91
|
+
outro(`Could not connect to ${base}: ${err.message}`);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
// Fetch config bundle (fall back to auth seed for older servers)
|
|
96
|
+
log.info('Fetching workspace config...');
|
|
97
|
+
const workspaceRoot = workspace.getWorkspacePath();
|
|
98
|
+
const sgDir = join(workspaceRoot, '.studiograph');
|
|
99
|
+
try {
|
|
100
|
+
const bundleRes = await fetch(`${base}/api/config/bundle`, {
|
|
101
|
+
headers: { 'Cookie': token },
|
|
102
|
+
});
|
|
103
|
+
if (bundleRes.ok) {
|
|
104
|
+
const bundle = await bundleRes.json();
|
|
105
|
+
// 1. Auth seed
|
|
106
|
+
writeFileSync(join(sgDir, 'auth-seed.json'), JSON.stringify(bundle.authSeed, null, 2), 'utf-8');
|
|
107
|
+
const authService = new AuthService(sgDir);
|
|
108
|
+
authService.applySeed();
|
|
109
|
+
authService.close();
|
|
110
|
+
log.success(`Team: ${bundle.authSeed.users.length} user${bundle.authSeed.users.length === 1 ? '' : 's'}`);
|
|
111
|
+
// 2. Workspace config — merge: keep local-only fields (server_url), update shared fields
|
|
112
|
+
if (bundle.workspace) {
|
|
113
|
+
const local = await workspace.loadConfig().catch(() => ({}));
|
|
114
|
+
const merged = { ...local, ...bundle.workspace };
|
|
115
|
+
if (local.server_url)
|
|
116
|
+
merged.server_url = local.server_url;
|
|
117
|
+
writeFileSync(join(sgDir, 'workspace.json'), JSON.stringify(merged, null, 2), 'utf-8');
|
|
118
|
+
log.success(`Workspace config: ${bundle.workspace.repos?.length ?? 0} collection${(bundle.workspace.repos?.length ?? 0) === 1 ? '' : 's'}`);
|
|
59
119
|
}
|
|
60
|
-
|
|
61
|
-
|
|
120
|
+
// 3. ABOUT.md
|
|
121
|
+
if (bundle.about !== null && bundle.about !== undefined) {
|
|
122
|
+
writeFileSync(join(sgDir, 'ABOUT.md'), bundle.about, 'utf-8');
|
|
123
|
+
log.success('Workspace identity (ABOUT.md)');
|
|
124
|
+
}
|
|
125
|
+
// 4. Sources
|
|
126
|
+
const sourceNames = Object.keys(bundle.sources ?? {});
|
|
127
|
+
if (sourceNames.length > 0) {
|
|
128
|
+
const sourcesDir = join(sgDir, 'sources');
|
|
129
|
+
mkdirSync(sourcesDir, { recursive: true });
|
|
130
|
+
for (const [name, def] of Object.entries(bundle.sources)) {
|
|
131
|
+
writeFileSync(join(sourcesDir, `${name}.json`), JSON.stringify(def, null, 2), 'utf-8');
|
|
132
|
+
}
|
|
133
|
+
log.success(`Sources: ${sourceNames.join(', ')}`);
|
|
134
|
+
}
|
|
135
|
+
// 5. Connectors (sanitized — no secrets)
|
|
136
|
+
if (bundle.connectors && bundle.connectors.length > 0) {
|
|
137
|
+
const connectorsDir = join(sgDir, 'connectors');
|
|
138
|
+
mkdirSync(connectorsDir, { recursive: true });
|
|
139
|
+
for (const conn of bundle.connectors) {
|
|
140
|
+
writeFileSync(join(connectorsDir, `${conn.name}.json`), JSON.stringify(conn, null, 2), 'utf-8');
|
|
141
|
+
}
|
|
142
|
+
log.success(`Connectors: ${bundle.connectors.map(c => c.name).join(', ')}`);
|
|
143
|
+
}
|
|
144
|
+
outro(`Pulled config from ${base}`);
|
|
145
|
+
}
|
|
146
|
+
else if (bundleRes.status === 404) {
|
|
147
|
+
// Older server — fall back to auth seed only
|
|
148
|
+
log.info('Server does not support config bundle, falling back to auth seed...');
|
|
149
|
+
const seedRes = await fetch(`${base}/api/auth/seed`, { headers: { 'Cookie': token } });
|
|
150
|
+
if (!seedRes.ok) {
|
|
151
|
+
const body = await seedRes.json().catch(() => ({}));
|
|
152
|
+
outro(`Failed to fetch seed: ${body.error || seedRes.statusText}`);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
return;
|
|
62
155
|
}
|
|
156
|
+
const seed = await seedRes.json();
|
|
157
|
+
writeFileSync(join(sgDir, 'auth-seed.json'), JSON.stringify(seed, null, 2), 'utf-8');
|
|
158
|
+
const authService = new AuthService(sgDir);
|
|
159
|
+
authService.applySeed();
|
|
160
|
+
authService.close();
|
|
161
|
+
outro(`Pulled ${seed.users.length} user${seed.users.length === 1 ? '' : 's'} from ${base}`);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
const body = await bundleRes.json().catch(() => ({}));
|
|
165
|
+
outro(`Failed: ${body.error || bundleRes.statusText}`);
|
|
166
|
+
process.exit(1);
|
|
63
167
|
}
|
|
64
168
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
function logManualGuidance(entryPath) {
|
|
70
|
-
log.info('');
|
|
71
|
-
log.info(' To resolve manually:');
|
|
72
|
-
log.info(` cd ${entryPath}`);
|
|
73
|
-
log.info(' git status');
|
|
74
|
-
log.info(' # edit conflicted files, then: git add <file> && git rebase --continue');
|
|
169
|
+
catch (err) {
|
|
170
|
+
outro(`Failed: ${err.message}`);
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
75
173
|
}
|
|
76
174
|
// ─── pull ─────────────────────────────────────────────────────────────────────
|
|
77
175
|
export const pullCommand = new Command('pull')
|
|
78
176
|
.description('Pull latest changes from remote across the workspace')
|
|
79
|
-
.
|
|
177
|
+
.option('--remote <url>', 'Pull config from a remote Studiograph server')
|
|
178
|
+
.action(async (opts) => {
|
|
179
|
+
if (opts.remote) {
|
|
180
|
+
return pullRemoteConfig(opts.remote);
|
|
181
|
+
}
|
|
80
182
|
const workspace = new Workspace();
|
|
81
183
|
if (!workspace.isWorkspace()) {
|
|
82
184
|
console.error('❌ Not a Studiograph workspace. Run `studiograph init` first.');
|
|
@@ -84,46 +186,132 @@ export const pullCommand = new Command('pull')
|
|
|
84
186
|
return;
|
|
85
187
|
}
|
|
86
188
|
const workspaceRoot = workspace.getWorkspacePath();
|
|
87
|
-
const entries = await getRepoEntries(workspace, workspaceRoot);
|
|
88
|
-
if (entries.length === 0) {
|
|
89
|
-
log.info('No collections with remotes to pull from.');
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
189
|
let hasConflict = false;
|
|
93
190
|
let hasError = false;
|
|
94
|
-
|
|
191
|
+
// Preserve server_url before pulling — it's local-only and gets overwritten
|
|
192
|
+
// when git pull brings down the config repo's workspace.json (which never has it).
|
|
193
|
+
const prePullConfig = await workspace.loadConfig().catch(() => ({}));
|
|
194
|
+
const savedServerUrl = prePullConfig.server_url;
|
|
195
|
+
// ── Step 1: pull .studiograph first so workspace.json is up to date ──────
|
|
196
|
+
const configPath = join(workspaceRoot, '.studiograph');
|
|
197
|
+
if (isGitRepo(configPath) && gitHasRemote(configPath)) {
|
|
95
198
|
try {
|
|
96
|
-
const result = gitPull(
|
|
199
|
+
const result = gitPull(configPath);
|
|
97
200
|
if (result === 'updated') {
|
|
98
|
-
log.success(
|
|
201
|
+
log.success('.studiograph: updated');
|
|
99
202
|
}
|
|
100
203
|
else if (result === 'up-to-date') {
|
|
101
|
-
log.info(
|
|
204
|
+
log.info('.studiograph: already up to date');
|
|
102
205
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
log.error(`.studiograph: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
209
|
+
hasError = true;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// ── Step 2: reload config — picks up any new collections added server-side ─
|
|
213
|
+
// Must use a fresh Workspace instance — loadConfig() caches this.config on
|
|
214
|
+
// first call, so the original instance would return stale data after the pull.
|
|
215
|
+
const freshWorkspace = new Workspace(workspaceRoot);
|
|
216
|
+
const config = await freshWorkspace.loadConfig();
|
|
217
|
+
// Re-apply server_url if the pull overwrote it (config repo never stores it).
|
|
218
|
+
if (savedServerUrl && !config.server_url) {
|
|
219
|
+
config.server_url = savedServerUrl;
|
|
220
|
+
freshWorkspace.writeConfig(config);
|
|
221
|
+
}
|
|
222
|
+
const serverUrl = (config.server_url ?? savedServerUrl)?.replace(/\/+$/, '');
|
|
223
|
+
// ── Step 3: pull or clone each collection ────────────────────────────────
|
|
224
|
+
// Resolve git credentials for the embedded server:
|
|
225
|
+
// 1. Stored JWT from `studiograph join` — per-user access control (preferred)
|
|
226
|
+
// 2. API key from auth-seed.json — admin access (fallback for workspace owners)
|
|
227
|
+
let serverGitCredentials;
|
|
228
|
+
if (serverUrl) {
|
|
229
|
+
// Try stored JWT first (saved by studiograph join)
|
|
230
|
+
const serverAuthPath = join(workspaceRoot, '.studiograph', 'server-auth.json');
|
|
231
|
+
if (existsSync(serverAuthPath)) {
|
|
232
|
+
try {
|
|
233
|
+
const serverAuth = JSON.parse(readFileSync(serverAuthPath, 'utf-8'));
|
|
234
|
+
if (serverAuth.email && serverAuth.jwtToken) {
|
|
235
|
+
serverGitCredentials = { username: serverAuth.email, password: serverAuth.jwtToken };
|
|
114
236
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
237
|
+
}
|
|
238
|
+
catch { /* non-fatal */ }
|
|
239
|
+
}
|
|
240
|
+
// Fall back to API key (used by workspace owners who set up with `studiograph serve`)
|
|
241
|
+
if (!serverGitCredentials) {
|
|
242
|
+
try {
|
|
243
|
+
const seedPath = join(workspaceRoot, '.studiograph', 'auth-seed.json');
|
|
244
|
+
if (existsSync(seedPath)) {
|
|
245
|
+
const seed = JSON.parse(readFileSync(seedPath, 'utf-8'));
|
|
246
|
+
const apiKey = seed.apiKey;
|
|
247
|
+
if (apiKey)
|
|
248
|
+
serverGitCredentials = { username: '_apikey_', password: apiKey };
|
|
119
249
|
}
|
|
120
250
|
}
|
|
251
|
+
catch { /* non-fatal */ }
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
for (const repo of config.repos) {
|
|
255
|
+
if (repo.private)
|
|
256
|
+
continue;
|
|
257
|
+
const repoPath = join(workspaceRoot, repo.path);
|
|
258
|
+
// New collection — directory missing or exists but isn't a git repo (e.g. failed clone)
|
|
259
|
+
if (!isGitRepo(repoPath)) {
|
|
260
|
+
const cloneUrl = serverUrl ? `${serverUrl}/git/${repo.name}` : null;
|
|
261
|
+
if (!cloneUrl) {
|
|
262
|
+
log.warn(`${repo.name}: new collection has no remote — skipping`);
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
const { gitClone } = await import('../../utils/git.js');
|
|
267
|
+
const { rmSync } = await import('fs');
|
|
268
|
+
// Remove any leftover empty/partial directory from a previous failed clone
|
|
269
|
+
if (existsSync(repoPath))
|
|
270
|
+
rmSync(repoPath, { recursive: true, force: true });
|
|
271
|
+
mkdirSync(repoPath, { recursive: true });
|
|
272
|
+
gitClone(cloneUrl, repoPath, { silent: true, credentials: serverGitCredentials });
|
|
273
|
+
log.success(`${repo.name}: cloned`);
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
log.error(`${repo.name}: clone failed — ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
277
|
+
hasError = true;
|
|
278
|
+
}
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
// Existing collection — pull if it has a remote
|
|
282
|
+
if (!isGitRepo(repoPath) || !gitHasRemote(repoPath))
|
|
283
|
+
continue;
|
|
284
|
+
try {
|
|
285
|
+
const result = gitPull(repoPath, { credentials: serverGitCredentials });
|
|
286
|
+
if (result === 'updated') {
|
|
287
|
+
log.success(`${repo.name}: updated`);
|
|
288
|
+
}
|
|
289
|
+
else if (result === 'up-to-date') {
|
|
290
|
+
log.info(`${repo.name}: already up to date`);
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
log.warn(`${repo.name}: diverged — has local commits not on the server`);
|
|
294
|
+
logManualGuidance(repoPath);
|
|
295
|
+
hasConflict = true;
|
|
296
|
+
}
|
|
121
297
|
}
|
|
122
298
|
catch (error) {
|
|
123
|
-
log.error(`${
|
|
299
|
+
log.error(`${repo.name}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
124
300
|
hasError = true;
|
|
125
301
|
}
|
|
126
302
|
}
|
|
303
|
+
// Apply auth seed if it was updated by pulling .studiograph
|
|
304
|
+
const seedPath = join(workspaceRoot, '.studiograph', 'auth-seed.json');
|
|
305
|
+
if (existsSync(seedPath)) {
|
|
306
|
+
try {
|
|
307
|
+
const authService = new AuthService(join(workspaceRoot, '.studiograph'));
|
|
308
|
+
authService.applySeed();
|
|
309
|
+
authService.close();
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
// Non-fatal — seed apply can fail if DB is locked by running server
|
|
313
|
+
}
|
|
314
|
+
}
|
|
127
315
|
if (hasConflict) {
|
|
128
316
|
outro('⚠️ Pull complete with conflicts. Resolve them, then run `studiograph push`.');
|
|
129
317
|
process.exit(1);
|
|
@@ -133,10 +321,156 @@ export const pullCommand = new Command('pull')
|
|
|
133
321
|
process.exit(1);
|
|
134
322
|
}
|
|
135
323
|
});
|
|
324
|
+
// ─── Remote config push ─────────────────────────────────────────────────────
|
|
325
|
+
async function pushRemoteConfig(remoteUrl) {
|
|
326
|
+
const workspace = new Workspace();
|
|
327
|
+
if (!workspace.isWorkspace()) {
|
|
328
|
+
console.error('❌ Not a Studiograph workspace. Run `studiograph init` first.');
|
|
329
|
+
process.exit(1);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const base = remoteUrl.replace(/\/+$/, '');
|
|
333
|
+
const workspaceRoot = workspace.getWorkspacePath();
|
|
334
|
+
const sgDir = join(workspaceRoot, '.studiograph');
|
|
335
|
+
// Prompt for admin credentials
|
|
336
|
+
const email = await text({ message: 'Admin email:', validate: (v) => v.includes('@') ? undefined : 'Enter a valid email' });
|
|
337
|
+
if (typeof email === 'symbol') {
|
|
338
|
+
outro('Cancelled');
|
|
339
|
+
process.exit(0);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const password = await passwordPrompt({ message: 'Password:' });
|
|
343
|
+
if (typeof password === 'symbol') {
|
|
344
|
+
outro('Cancelled');
|
|
345
|
+
process.exit(0);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
// Login
|
|
349
|
+
log.info('Authenticating...');
|
|
350
|
+
let token;
|
|
351
|
+
try {
|
|
352
|
+
const loginRes = await fetch(`${base}/api/auth/login`, {
|
|
353
|
+
method: 'POST',
|
|
354
|
+
headers: { 'Content-Type': 'application/json' },
|
|
355
|
+
body: JSON.stringify({ email, password }),
|
|
356
|
+
});
|
|
357
|
+
if (!loginRes.ok) {
|
|
358
|
+
const body = await loginRes.json().catch(() => ({}));
|
|
359
|
+
outro(`Login failed: ${body.error || loginRes.statusText}`);
|
|
360
|
+
process.exit(1);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const cookies = loginRes.headers.getSetCookie?.() ?? [];
|
|
364
|
+
const tokenCookie = cookies.find(c => c.startsWith('__sg_token='));
|
|
365
|
+
if (!tokenCookie) {
|
|
366
|
+
outro('Login succeeded but no session cookie was returned');
|
|
367
|
+
process.exit(1);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
token = tokenCookie.split(';')[0];
|
|
371
|
+
}
|
|
372
|
+
catch (err) {
|
|
373
|
+
outro(`Could not connect to ${base}: ${err.message}`);
|
|
374
|
+
process.exit(1);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
// Build config bundle from local workspace
|
|
378
|
+
log.info('Building config bundle...');
|
|
379
|
+
const config = await workspace.loadConfig();
|
|
380
|
+
const about = workspace.loadAbout();
|
|
381
|
+
// Auth seed
|
|
382
|
+
let authSeed;
|
|
383
|
+
const seedPath = join(sgDir, 'auth-seed.json');
|
|
384
|
+
if (existsSync(seedPath)) {
|
|
385
|
+
try {
|
|
386
|
+
authSeed = JSON.parse(readFileSync(seedPath, 'utf-8'));
|
|
387
|
+
}
|
|
388
|
+
catch { /* skip */ }
|
|
389
|
+
}
|
|
390
|
+
// Sources
|
|
391
|
+
const sources = {};
|
|
392
|
+
const sourcesDir = join(sgDir, 'sources');
|
|
393
|
+
if (existsSync(sourcesDir)) {
|
|
394
|
+
for (const entry of readdirSync(sourcesDir)) {
|
|
395
|
+
if (!entry.endsWith('.json'))
|
|
396
|
+
continue;
|
|
397
|
+
try {
|
|
398
|
+
sources[entry.replace('.json', '')] = JSON.parse(readFileSync(join(sourcesDir, entry), 'utf-8'));
|
|
399
|
+
}
|
|
400
|
+
catch { /* skip */ }
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
// Connectors (workspace-level, already sanitized)
|
|
404
|
+
const connectors = ConnectorManager.loadWorkspaceConfigs(workspaceRoot);
|
|
405
|
+
const bundle = { workspace: config, about, authSeed, sources, connectors };
|
|
406
|
+
// Push to remote
|
|
407
|
+
log.info('Pushing config...');
|
|
408
|
+
try {
|
|
409
|
+
const res = await fetch(`${base}/api/config/bundle`, {
|
|
410
|
+
method: 'POST',
|
|
411
|
+
headers: { 'Content-Type': 'application/json', 'Cookie': token },
|
|
412
|
+
body: JSON.stringify(bundle),
|
|
413
|
+
});
|
|
414
|
+
if (!res.ok) {
|
|
415
|
+
const body = await res.json().catch(() => ({}));
|
|
416
|
+
outro(`Failed: ${body.error || res.statusText}`);
|
|
417
|
+
process.exit(1);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
const result = await res.json();
|
|
421
|
+
if (result.applied?.length) {
|
|
422
|
+
for (const item of result.applied) {
|
|
423
|
+
log.success(item);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
outro(`Pushed config to ${base}`);
|
|
427
|
+
}
|
|
428
|
+
catch (err) {
|
|
429
|
+
outro(`Failed: ${err.message}`);
|
|
430
|
+
process.exit(1);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
// ─── Server-aware push helpers ───────────────────────────────────────────────
|
|
434
|
+
function makeBasicAuth(username, password) {
|
|
435
|
+
return 'Basic ' + Buffer.from(`${username}:${password}`).toString('base64');
|
|
436
|
+
}
|
|
437
|
+
async function serverFetch(serverUrl, path, authHeader) {
|
|
438
|
+
const res = await fetch(`${serverUrl}${path}`, {
|
|
439
|
+
headers: {
|
|
440
|
+
'Authorization': authHeader,
|
|
441
|
+
'Accept': 'application/json',
|
|
442
|
+
},
|
|
443
|
+
});
|
|
444
|
+
if (!res.ok) {
|
|
445
|
+
if (res.status === 401)
|
|
446
|
+
throw new Error('Invalid credentials');
|
|
447
|
+
const body = await res.json().catch(() => ({}));
|
|
448
|
+
throw new Error(body.error || `Server returned ${res.status}`);
|
|
449
|
+
}
|
|
450
|
+
return res.json();
|
|
451
|
+
}
|
|
452
|
+
async function ensureServerCollection(serverUrl, repoName, isPrivate, authHeader) {
|
|
453
|
+
const res = await fetch(`${serverUrl}/api/repos`, {
|
|
454
|
+
method: 'POST',
|
|
455
|
+
headers: { 'Authorization': authHeader, 'Content-Type': 'application/json' },
|
|
456
|
+
body: JSON.stringify({ name: repoName, ...(isPrivate ? { private: true } : {}) }),
|
|
457
|
+
});
|
|
458
|
+
if (res.ok)
|
|
459
|
+
return 'created';
|
|
460
|
+
if (res.status === 409)
|
|
461
|
+
return 'exists'; // already exists
|
|
462
|
+
const body = await res.json().catch(() => ({}));
|
|
463
|
+
throw new Error(body.error || `Failed to create collection "${repoName}": ${res.statusText}`);
|
|
464
|
+
}
|
|
136
465
|
// ─── push ─────────────────────────────────────────────────────────────────────
|
|
137
466
|
export const pushCommand = new Command('push')
|
|
138
467
|
.description('Push local commits to remote across the workspace')
|
|
139
|
-
.
|
|
468
|
+
.argument('[collection]', 'Push a specific collection (default: all)')
|
|
469
|
+
.option('--remote <url>', 'Push config to a remote Studiograph server')
|
|
470
|
+
.action(async (collection, opts) => {
|
|
471
|
+
if (opts.remote) {
|
|
472
|
+
return pushRemoteConfig(opts.remote);
|
|
473
|
+
}
|
|
140
474
|
const workspace = new Workspace();
|
|
141
475
|
if (!workspace.isWorkspace()) {
|
|
142
476
|
console.error('❌ Not a Studiograph workspace. Run `studiograph init` first.');
|
|
@@ -144,37 +478,139 @@ export const pushCommand = new Command('push')
|
|
|
144
478
|
return;
|
|
145
479
|
}
|
|
146
480
|
const workspaceRoot = workspace.getWorkspacePath();
|
|
481
|
+
const config = await workspace.loadConfig();
|
|
482
|
+
const serverUrl = config.server_url?.replace(/\/+$/, '');
|
|
483
|
+
// Build list of repos to push
|
|
484
|
+
let repos = config.repos.filter(r => !r.private || serverUrl); // include private only when pushing to server
|
|
485
|
+
if (collection) {
|
|
486
|
+
const match = repos.find(r => r.name === collection);
|
|
487
|
+
if (!match) {
|
|
488
|
+
log.error(`Collection "${collection}" not found in workspace.`);
|
|
489
|
+
process.exit(1);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
repos = [match];
|
|
493
|
+
}
|
|
494
|
+
// Server-aware push: workspace has a server_url (set by `studiograph join`)
|
|
495
|
+
if (serverUrl) {
|
|
496
|
+
// Resolve credentials: JWT (per-user) → API key (admin) → prompt
|
|
497
|
+
let authHeader;
|
|
498
|
+
let gitCredentials;
|
|
499
|
+
// 1. Stored JWT from `studiograph join` (per-user access control)
|
|
500
|
+
const pushServerAuthPath = join(workspaceRoot, '.studiograph', 'server-auth.json');
|
|
501
|
+
try {
|
|
502
|
+
if (existsSync(pushServerAuthPath)) {
|
|
503
|
+
const sa = JSON.parse(readFileSync(pushServerAuthPath, 'utf-8'));
|
|
504
|
+
if (sa.email && sa.jwtToken) {
|
|
505
|
+
authHeader = makeBasicAuth(sa.email, sa.jwtToken);
|
|
506
|
+
gitCredentials = { username: sa.email, password: sa.jwtToken };
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
catch { /* non-fatal */ }
|
|
511
|
+
// 2. API key fallback (workspace owners who set up with `studiograph serve`)
|
|
512
|
+
if (!authHeader) {
|
|
513
|
+
try {
|
|
514
|
+
const seedPathPush = join(workspaceRoot, '.studiograph', 'auth-seed.json');
|
|
515
|
+
if (existsSync(seedPathPush)) {
|
|
516
|
+
const seed = JSON.parse(readFileSync(seedPathPush, 'utf-8'));
|
|
517
|
+
const pushApiKey = seed.apiKey;
|
|
518
|
+
if (pushApiKey) {
|
|
519
|
+
authHeader = makeBasicAuth('_apikey_', pushApiKey);
|
|
520
|
+
gitCredentials = { username: '_apikey_', password: pushApiKey };
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
catch { /* non-fatal */ }
|
|
525
|
+
}
|
|
526
|
+
// 3. Interactive credential prompt as last resort
|
|
527
|
+
if (!authHeader) {
|
|
528
|
+
const email = await text({ message: 'Email:', validate: (v) => v.includes('@') ? undefined : 'Enter a valid email' });
|
|
529
|
+
if (typeof email === 'symbol') {
|
|
530
|
+
outro('Cancelled');
|
|
531
|
+
process.exit(0);
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
const pass = await passwordPrompt({ message: 'Password:' });
|
|
535
|
+
if (typeof pass === 'symbol') {
|
|
536
|
+
outro('Cancelled');
|
|
537
|
+
process.exit(0);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
authHeader = makeBasicAuth(String(email), String(pass));
|
|
541
|
+
gitCredentials = { username: String(email), password: String(pass) };
|
|
542
|
+
}
|
|
543
|
+
// All three branches above either set credentials or exit — this is a safety guard
|
|
544
|
+
if (!authHeader || !gitCredentials) {
|
|
545
|
+
outro('Could not resolve credentials');
|
|
546
|
+
process.exit(1);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
// Validate credentials
|
|
550
|
+
try {
|
|
551
|
+
await serverFetch(serverUrl, '/api/workspace', authHeader);
|
|
552
|
+
}
|
|
553
|
+
catch (err) {
|
|
554
|
+
outro(`Authentication failed: ${err.message}`);
|
|
555
|
+
process.exit(1);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
let hasError = false;
|
|
559
|
+
for (const repo of repos) {
|
|
560
|
+
const repoPath = join(workspaceRoot, repo.path);
|
|
561
|
+
if (!isGitRepo(repoPath)) {
|
|
562
|
+
log.warn(`${repo.name}: not a git repo, skipping`);
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
try {
|
|
566
|
+
// Ensure collection exists on server
|
|
567
|
+
const status = await ensureServerCollection(serverUrl, repo.name, !!repo.private, authHeader);
|
|
568
|
+
if (status === 'created') {
|
|
569
|
+
log.info(`${repo.name}: created on server`);
|
|
570
|
+
}
|
|
571
|
+
// Set origin to server git URL
|
|
572
|
+
gitSetRemote(repoPath, `${serverUrl}/git/${repo.name}`);
|
|
573
|
+
// Push with credentials
|
|
574
|
+
let result = gitPushWithCredentials(repoPath, gitCredentials);
|
|
575
|
+
if (result === 'rejected') {
|
|
576
|
+
log.warn(`${repo.name}: push rejected — run \`studiograph pull\` first`);
|
|
577
|
+
hasError = true;
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
if (result === 'pushed') {
|
|
581
|
+
log.success(`${repo.name}: pushed`);
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
log.info(`${repo.name}: nothing to push`);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
catch (error) {
|
|
588
|
+
log.error(`${repo.name}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
589
|
+
hasError = true;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
if (hasError) {
|
|
593
|
+
outro('Push completed with errors.');
|
|
594
|
+
process.exit(1);
|
|
595
|
+
}
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
// Fallback: plain git push for repos that already have remotes
|
|
147
599
|
const entries = await getRepoEntries(workspace, workspaceRoot);
|
|
148
600
|
if (entries.length === 0) {
|
|
149
|
-
log.info('No collections with remotes to push to.');
|
|
601
|
+
log.info('No collections with remotes to push to. Use `studiograph join` to connect to a server.');
|
|
150
602
|
return;
|
|
151
603
|
}
|
|
152
604
|
let hasError = false;
|
|
153
605
|
for (const entry of entries) {
|
|
606
|
+
if (collection && entry.name !== collection)
|
|
607
|
+
continue;
|
|
154
608
|
try {
|
|
155
609
|
let result = gitPush(entry.path);
|
|
156
|
-
// Auto pull-rebase and retry if remote has newer commits
|
|
157
610
|
if (result === 'rejected') {
|
|
158
|
-
log.
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const resolution = await resolveConflicts(entry.path, (msg) => log.info(msg));
|
|
162
|
-
gitStashPop(entry.path); // restore any auto-stashed local changes
|
|
163
|
-
if (!resolution.success) {
|
|
164
|
-
logResolutions(entry.name, resolution);
|
|
165
|
-
logManualGuidance(entry.path);
|
|
166
|
-
hasError = true;
|
|
167
|
-
continue;
|
|
168
|
-
}
|
|
169
|
-
logResolutions(entry.name, resolution);
|
|
170
|
-
}
|
|
171
|
-
// Retry push after successful pull
|
|
172
|
-
result = gitPush(entry.path);
|
|
173
|
-
if (result === 'rejected') {
|
|
174
|
-
log.warn(`${entry.name}: push still rejected after pull`);
|
|
175
|
-
hasError = true;
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
611
|
+
log.warn(`${entry.name}: push rejected — run \`studiograph pull\` first`);
|
|
612
|
+
hasError = true;
|
|
613
|
+
continue;
|
|
178
614
|
}
|
|
179
615
|
if (result === 'pushed') {
|
|
180
616
|
log.success(`${entry.name}: pushed`);
|
|
@@ -189,7 +625,7 @@ export const pushCommand = new Command('push')
|
|
|
189
625
|
}
|
|
190
626
|
}
|
|
191
627
|
if (hasError) {
|
|
192
|
-
outro('
|
|
628
|
+
outro('Push failed for one or more collections.');
|
|
193
629
|
process.exit(1);
|
|
194
630
|
}
|
|
195
631
|
});
|