studiograph 1.3.3 → 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 +10 -0
- package/dist/agent/orchestrator.js +26 -7
- 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/graph-tools.d.ts +5 -1
- package/dist/agent/tools/graph-tools.js +161 -9
- package/dist/agent/tools/graph-tools.js.map +1 -1
- package/dist/agent/tools/ops-tools.js +15 -126
- package/dist/agent/tools/ops-tools.js.map +1 -1
- package/dist/agent/tools/permission-tools.d.ts +15 -14
- package/dist/agent/tools/permission-tools.js +65 -128
- package/dist/agent/tools/permission-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.d.ts +3 -2
- package/dist/cli/commands/join.js +142 -97
- 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.d.ts +4 -2
- package/dist/cli/commands/sync.js +529 -94
- package/dist/cli/commands/sync.js.map +1 -1
- package/dist/cli/index.js +15 -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/graph.d.ts +8 -2
- package/dist/core/graph.js +11 -7
- package/dist/core/graph.js.map +1 -1
- package/dist/core/types.d.ts +149 -21
- package/dist/core/types.js +16 -4
- package/dist/core/types.js.map +1 -1
- package/dist/core/workspace-manager.js +1 -5
- package/dist/core/workspace-manager.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 +46 -220
- 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 +39 -0
- package/dist/server/commit-scheduler.js +113 -0
- package/dist/server/commit-scheduler.js.map +1 -0
- package/dist/server/index.d.ts +0 -2
- package/dist/server/index.js +166 -55
- 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 +78 -0
- package/dist/server/routes/auth-api.js.map +1 -1
- package/dist/server/routes/chat.js +4 -0
- package/dist/server/routes/chat.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 +23 -0
- package/dist/server/routes/git-http.js +251 -0
- package/dist/server/routes/git-http.js.map +1 -0
- package/dist/server/routes/graph-api.d.ts +6 -2
- package/dist/server/routes/graph-api.js +266 -82
- 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 +6 -4
- package/dist/server/routes/permissions-api.js +53 -167
- 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 +9 -0
- package/dist/server/routes/ws.js +131 -0
- package/dist/server/routes/ws.js.map +1 -0
- 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 +130 -0
- package/dist/server/ws-hub.js +250 -0
- package/dist/server/ws-hub.js.map +1 -0
- 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 +74 -0
- package/dist/services/auth-service.js +286 -6
- 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 +33 -20
- package/dist/utils/git.js +119 -62
- 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.BYpFDZHK.css +1 -0
- 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/{5.BBpmYkAu.js → 4.Dp0Z-oPW.js} +2 -2
- 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 +12 -2
- package/dist/web/_app/immutable/assets/0.CDbX4Cwz.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/AppShell.D0rmbdqF.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/479TgXB4.js +0 -1
- package/dist/web/_app/immutable/chunks/4QY4j-jX.js +0 -1
- package/dist/web/_app/immutable/chunks/BFb0g4TQ.js +0 -64
- package/dist/web/_app/immutable/chunks/Bopa-Ask.js +0 -1
- package/dist/web/_app/immutable/chunks/COwytaCP.js +0 -1
- package/dist/web/_app/immutable/chunks/DEJSHbC3.js +0 -1
- package/dist/web/_app/immutable/chunks/DNywhIex.js +0 -23
- package/dist/web/_app/immutable/chunks/DTUXhwEY.js +0 -1
- package/dist/web/_app/immutable/chunks/DThXpa0U.js +0 -6
- package/dist/web/_app/immutable/chunks/Dh_H7Owr.js +0 -18
- package/dist/web/_app/immutable/chunks/Dml-u95b.js +0 -2
- 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/DvKVaE7M.js +0 -1
- package/dist/web/_app/immutable/chunks/MbiSz-iW.js +0 -2
- package/dist/web/_app/immutable/chunks/bSAC733J.js +0 -1
- package/dist/web/_app/immutable/entry/app.BvodXQQ0.js +0 -2
- package/dist/web/_app/immutable/entry/start.Bkui3Kyw.js +0 -1
- package/dist/web/_app/immutable/nodes/0.DfbCOBhn.js +0 -2
- package/dist/web/_app/immutable/nodes/1.vtxUGpe6.js +0 -1
- package/dist/web/_app/immutable/nodes/2.Cq29oW4h.js +0 -1
- package/dist/web/_app/immutable/nodes/3.SquslPZy.js +0 -1
- package/dist/web/_app/immutable/nodes/4.COV8FR8b.js +0 -16
- package/dist/web/_app/immutable/nodes/6.BBbh6z9I.js +0 -2
- /package/dist/web/_app/immutable/assets/{5.BhKgiXd2.css → 4.BhKgiXd2.css} +0 -0
|
@@ -2,25 +2,33 @@
|
|
|
2
2
|
* studiograph pull / push commands
|
|
3
3
|
*
|
|
4
4
|
* Pull and push changes across the workspace:
|
|
5
|
-
* - the .studiograph config repo
|
|
6
|
-
* - each entity
|
|
5
|
+
* - the .studiograph config repo (if it has a git remote)
|
|
6
|
+
* - each entity collection that is a git repo with a remote
|
|
7
|
+
*
|
|
8
|
+
* Works with both GitHub remotes and the embedded git server.
|
|
7
9
|
*/
|
|
8
10
|
import { Command } from 'commander';
|
|
9
|
-
import { log, outro } from '@clack/prompts';
|
|
10
|
-
import { existsSync } from 'fs';
|
|
11
|
+
import { log, outro, text, password as passwordPrompt } from '@clack/prompts';
|
|
12
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'fs';
|
|
11
13
|
import { join } from 'path';
|
|
12
14
|
import { Workspace } from '../../core/workspace.js';
|
|
13
|
-
import {
|
|
14
|
-
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';
|
|
15
18
|
async function getRepoEntries(workspace, workspaceRoot) {
|
|
16
19
|
const entries = [];
|
|
17
20
|
// The .studiograph config directory is itself a git repo (when GitHub-provisioned)
|
|
18
|
-
|
|
21
|
+
const configPath = join(workspaceRoot, '.studiograph');
|
|
22
|
+
if (isGitRepo(configPath) && gitHasRemote(configPath)) {
|
|
23
|
+
entries.push({ name: '.studiograph', path: configPath });
|
|
24
|
+
}
|
|
19
25
|
const config = await workspace.loadConfig();
|
|
20
26
|
for (const repo of config.repos) {
|
|
21
|
-
if (
|
|
22
|
-
continue; //
|
|
27
|
+
if (repo.private)
|
|
28
|
+
continue; // private repos are local-only
|
|
23
29
|
const repoPath = join(workspaceRoot, repo.path);
|
|
30
|
+
if (!isGitRepo(repoPath) || !gitHasRemote(repoPath))
|
|
31
|
+
continue;
|
|
24
32
|
entries.push({ name: repo.name, path: repoPath });
|
|
25
33
|
}
|
|
26
34
|
return entries;
|
|
@@ -29,47 +37,148 @@ function isGitRepo(path) {
|
|
|
29
37
|
return existsSync(path) && existsSync(join(path, '.git'));
|
|
30
38
|
}
|
|
31
39
|
/**
|
|
32
|
-
* Print
|
|
40
|
+
* Print manual resolution guidance when pull has diverged.
|
|
33
41
|
*/
|
|
34
|
-
function
|
|
35
|
-
|
|
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);
|
|
36
50
|
return;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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;
|
|
44
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;
|
|
87
|
+
}
|
|
88
|
+
token = tokenCookie.split(';')[0];
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
outro(`Could not connect to ${base}: ${err.message}`);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
return;
|
|
45
94
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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'}`);
|
|
52
119
|
}
|
|
53
|
-
|
|
54
|
-
|
|
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;
|
|
55
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);
|
|
56
167
|
}
|
|
57
168
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
function logManualGuidance(entryPath) {
|
|
63
|
-
log.info('');
|
|
64
|
-
log.info(' To resolve manually:');
|
|
65
|
-
log.info(` cd ${entryPath}`);
|
|
66
|
-
log.info(' git status');
|
|
67
|
-
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
|
+
}
|
|
68
173
|
}
|
|
69
174
|
// ─── pull ─────────────────────────────────────────────────────────────────────
|
|
70
175
|
export const pullCommand = new Command('pull')
|
|
71
176
|
.description('Pull latest changes from remote across the workspace')
|
|
72
|
-
.
|
|
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
|
+
}
|
|
73
182
|
const workspace = new Workspace();
|
|
74
183
|
if (!workspace.isWorkspace()) {
|
|
75
184
|
console.error('❌ Not a Studiograph workspace. Run `studiograph init` first.');
|
|
@@ -77,50 +186,132 @@ export const pullCommand = new Command('pull')
|
|
|
77
186
|
return;
|
|
78
187
|
}
|
|
79
188
|
const workspaceRoot = workspace.getWorkspacePath();
|
|
80
|
-
const entries = await getRepoEntries(workspace, workspaceRoot);
|
|
81
189
|
let hasConflict = false;
|
|
82
190
|
let hasError = false;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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)) {
|
|
198
|
+
try {
|
|
199
|
+
const result = gitPull(configPath);
|
|
200
|
+
if (result === 'updated') {
|
|
201
|
+
log.success('.studiograph: updated');
|
|
202
|
+
}
|
|
203
|
+
else if (result === 'up-to-date') {
|
|
204
|
+
log.info('.studiograph: already up to date');
|
|
205
|
+
}
|
|
87
206
|
}
|
|
88
|
-
|
|
89
|
-
log.
|
|
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 };
|
|
236
|
+
}
|
|
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 };
|
|
249
|
+
}
|
|
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
|
+
}
|
|
90
279
|
continue;
|
|
91
280
|
}
|
|
281
|
+
// Existing collection — pull if it has a remote
|
|
282
|
+
if (!isGitRepo(repoPath) || !gitHasRemote(repoPath))
|
|
283
|
+
continue;
|
|
92
284
|
try {
|
|
93
|
-
const result = gitPull(
|
|
285
|
+
const result = gitPull(repoPath, { credentials: serverGitCredentials });
|
|
94
286
|
if (result === 'updated') {
|
|
95
|
-
log.success(`${
|
|
287
|
+
log.success(`${repo.name}: updated`);
|
|
96
288
|
}
|
|
97
289
|
else if (result === 'up-to-date') {
|
|
98
|
-
log.info(`${
|
|
290
|
+
log.info(`${repo.name}: already up to date`);
|
|
99
291
|
}
|
|
100
292
|
else {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (resolution.success) {
|
|
105
|
-
logResolutions(entry.name, resolution);
|
|
106
|
-
}
|
|
107
|
-
else if (resolution.resolutions.length > 0) {
|
|
108
|
-
logResolutions(entry.name, resolution);
|
|
109
|
-
logManualGuidance(entry.path);
|
|
110
|
-
hasConflict = true;
|
|
111
|
-
}
|
|
112
|
-
else {
|
|
113
|
-
log.warn(`${entry.name}: conflict — resolve manually in ${entry.path}`);
|
|
114
|
-
logManualGuidance(entry.path);
|
|
115
|
-
hasConflict = true;
|
|
116
|
-
}
|
|
293
|
+
log.warn(`${repo.name}: diverged — has local commits not on the server`);
|
|
294
|
+
logManualGuidance(repoPath);
|
|
295
|
+
hasConflict = true;
|
|
117
296
|
}
|
|
118
297
|
}
|
|
119
298
|
catch (error) {
|
|
120
|
-
log.error(`${
|
|
299
|
+
log.error(`${repo.name}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
121
300
|
hasError = true;
|
|
122
301
|
}
|
|
123
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
|
+
}
|
|
124
315
|
if (hasConflict) {
|
|
125
316
|
outro('⚠️ Pull complete with conflicts. Resolve them, then run `studiograph push`.');
|
|
126
317
|
process.exit(1);
|
|
@@ -130,10 +321,156 @@ export const pullCommand = new Command('pull')
|
|
|
130
321
|
process.exit(1);
|
|
131
322
|
}
|
|
132
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
|
+
}
|
|
133
465
|
// ─── push ─────────────────────────────────────────────────────────────────────
|
|
134
466
|
export const pushCommand = new Command('push')
|
|
135
467
|
.description('Push local commits to remote across the workspace')
|
|
136
|
-
.
|
|
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
|
+
}
|
|
137
474
|
const workspace = new Workspace();
|
|
138
475
|
if (!workspace.isWorkspace()) {
|
|
139
476
|
console.error('❌ Not a Studiograph workspace. Run `studiograph init` first.');
|
|
@@ -141,41 +478,139 @@ export const pushCommand = new Command('push')
|
|
|
141
478
|
return;
|
|
142
479
|
}
|
|
143
480
|
const workspaceRoot = workspace.getWorkspacePath();
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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;
|
|
154
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');
|
|
155
501
|
try {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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 };
|
|
169
521
|
}
|
|
170
|
-
logResolutions(entry.name, resolution);
|
|
171
522
|
}
|
|
172
|
-
|
|
173
|
-
|
|
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);
|
|
174
575
|
if (result === 'rejected') {
|
|
175
|
-
log.warn(`${
|
|
576
|
+
log.warn(`${repo.name}: push rejected — run \`studiograph pull\` first`);
|
|
176
577
|
hasError = true;
|
|
177
578
|
continue;
|
|
178
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
|
|
599
|
+
const entries = await getRepoEntries(workspace, workspaceRoot);
|
|
600
|
+
if (entries.length === 0) {
|
|
601
|
+
log.info('No collections with remotes to push to. Use `studiograph join` to connect to a server.');
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
let hasError = false;
|
|
605
|
+
for (const entry of entries) {
|
|
606
|
+
if (collection && entry.name !== collection)
|
|
607
|
+
continue;
|
|
608
|
+
try {
|
|
609
|
+
let result = gitPush(entry.path);
|
|
610
|
+
if (result === 'rejected') {
|
|
611
|
+
log.warn(`${entry.name}: push rejected — run \`studiograph pull\` first`);
|
|
612
|
+
hasError = true;
|
|
613
|
+
continue;
|
|
179
614
|
}
|
|
180
615
|
if (result === 'pushed') {
|
|
181
616
|
log.success(`${entry.name}: pushed`);
|
|
@@ -190,7 +625,7 @@ export const pushCommand = new Command('push')
|
|
|
190
625
|
}
|
|
191
626
|
}
|
|
192
627
|
if (hasError) {
|
|
193
|
-
outro('
|
|
628
|
+
outro('Push failed for one or more collections.');
|
|
194
629
|
process.exit(1);
|
|
195
630
|
}
|
|
196
631
|
});
|