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.
Files changed (282) hide show
  1. package/dist/agent/orchestrator.d.ts +2 -0
  2. package/dist/agent/orchestrator.js +13 -5
  3. package/dist/agent/orchestrator.js.map +1 -1
  4. package/dist/agent/skills/sync-configuration.md +4 -29
  5. package/dist/agent/skills/sync-setup.md +2 -4
  6. package/dist/agent/tools/ops-tools.js +15 -126
  7. package/dist/agent/tools/ops-tools.js.map +1 -1
  8. package/dist/agent/tools/sync-tools.d.ts +7 -6
  9. package/dist/agent/tools/sync-tools.js +205 -178
  10. package/dist/agent/tools/sync-tools.js.map +1 -1
  11. package/dist/cli/commands/about.d.ts +13 -0
  12. package/dist/cli/commands/about.js +97 -0
  13. package/dist/cli/commands/about.js.map +1 -0
  14. package/dist/cli/commands/clone.d.ts +5 -2
  15. package/dist/cli/commands/clone.js +131 -62
  16. package/dist/cli/commands/clone.js.map +1 -1
  17. package/dist/cli/commands/connector.d.ts +2 -16
  18. package/dist/cli/commands/connector.js +32 -109
  19. package/dist/cli/commands/connector.js.map +1 -1
  20. package/dist/cli/commands/deploy.d.ts +0 -1
  21. package/dist/cli/commands/deploy.js +13 -103
  22. package/dist/cli/commands/deploy.js.map +1 -1
  23. package/dist/cli/commands/init.js +6 -93
  24. package/dist/cli/commands/init.js.map +1 -1
  25. package/dist/cli/commands/join.js +51 -1
  26. package/dist/cli/commands/join.js.map +1 -1
  27. package/dist/cli/commands/redeploy.js +1 -2
  28. package/dist/cli/commands/redeploy.js.map +1 -1
  29. package/dist/cli/commands/serve.d.ts +1 -3
  30. package/dist/cli/commands/serve.js +29 -109
  31. package/dist/cli/commands/serve.js.map +1 -1
  32. package/dist/cli/commands/start.js +1 -1
  33. package/dist/cli/commands/start.js.map +1 -1
  34. package/dist/cli/commands/sync-collection.d.ts +14 -0
  35. package/dist/cli/commands/sync-collection.js +366 -0
  36. package/dist/cli/commands/sync-collection.js.map +1 -0
  37. package/dist/cli/commands/sync.js +518 -82
  38. package/dist/cli/commands/sync.js.map +1 -1
  39. package/dist/cli/index.js +12 -30
  40. package/dist/cli/index.js.map +1 -1
  41. package/dist/cli/setup-wizard.d.ts +0 -13
  42. package/dist/cli/setup-wizard.js +6 -81
  43. package/dist/cli/setup-wizard.js.map +1 -1
  44. package/dist/core/types.d.ts +140 -21
  45. package/dist/core/types.js +15 -4
  46. package/dist/core/types.js.map +1 -1
  47. package/dist/core/workspace.d.ts +11 -4
  48. package/dist/core/workspace.js +61 -26
  49. package/dist/core/workspace.js.map +1 -1
  50. package/dist/integrations/asana.d.ts +26 -0
  51. package/dist/integrations/asana.js +77 -0
  52. package/dist/integrations/asana.js.map +1 -0
  53. package/dist/integrations/figma-local.d.ts +16 -0
  54. package/dist/integrations/figma-local.js +10 -0
  55. package/dist/integrations/figma-local.js.map +1 -0
  56. package/dist/integrations/figma.d.ts +17 -0
  57. package/dist/integrations/figma.js +16 -0
  58. package/dist/integrations/figma.js.map +1 -0
  59. package/dist/integrations/granola.d.ts +24 -0
  60. package/dist/integrations/granola.js +59 -0
  61. package/dist/integrations/granola.js.map +1 -0
  62. package/dist/integrations/linear.d.ts +14 -0
  63. package/dist/integrations/linear.js +70 -0
  64. package/dist/integrations/linear.js.map +1 -0
  65. package/dist/integrations/paper-local.d.ts +14 -0
  66. package/dist/integrations/paper-local.js +10 -0
  67. package/dist/integrations/paper-local.js.map +1 -0
  68. package/dist/integrations/paper.d.ts +2 -0
  69. package/dist/integrations/paper.js +10 -0
  70. package/dist/integrations/paper.js.map +1 -0
  71. package/dist/integrations/pipedrive.d.ts +26 -0
  72. package/dist/integrations/pipedrive.js +97 -0
  73. package/dist/integrations/pipedrive.js.map +1 -0
  74. package/dist/integrations/registry.d.ts +15 -0
  75. package/dist/integrations/registry.js +27 -0
  76. package/dist/integrations/registry.js.map +1 -0
  77. package/dist/integrations/types.d.ts +34 -0
  78. package/dist/integrations/types.js +9 -0
  79. package/dist/integrations/types.js.map +1 -0
  80. package/dist/mcp/connector-manager.d.ts +45 -31
  81. package/dist/mcp/connector-manager.js +164 -116
  82. package/dist/mcp/connector-manager.js.map +1 -1
  83. package/dist/mcp/server-oauth-provider.d.ts +56 -0
  84. package/dist/mcp/server-oauth-provider.js +138 -0
  85. package/dist/mcp/server-oauth-provider.js.map +1 -0
  86. package/dist/server/chrome/chrome.css +142 -28
  87. package/dist/server/chrome/chrome.js +51 -25
  88. package/dist/server/collab-authority.d.ts +70 -0
  89. package/dist/server/collab-authority.js +218 -0
  90. package/dist/server/collab-authority.js.map +1 -0
  91. package/dist/server/collab.d.ts +29 -0
  92. package/dist/server/collab.js +195 -0
  93. package/dist/server/collab.js.map +1 -0
  94. package/dist/server/commit-scheduler.d.ts +1 -1
  95. package/dist/server/commit-scheduler.js +18 -4
  96. package/dist/server/commit-scheduler.js.map +1 -1
  97. package/dist/server/index.d.ts +0 -2
  98. package/dist/server/index.js +89 -18
  99. package/dist/server/index.js.map +1 -1
  100. package/dist/server/routes/auth-api.d.ts +6 -0
  101. package/dist/server/routes/auth-api.js +55 -0
  102. package/dist/server/routes/auth-api.js.map +1 -1
  103. package/dist/server/routes/collab.d.ts +6 -0
  104. package/dist/server/routes/collab.js +10 -0
  105. package/dist/server/routes/collab.js.map +1 -0
  106. package/dist/server/routes/git-http.d.ts +1 -1
  107. package/dist/server/routes/git-http.js +53 -15
  108. package/dist/server/routes/git-http.js.map +1 -1
  109. package/dist/server/routes/graph-api.d.ts +2 -2
  110. package/dist/server/routes/graph-api.js +59 -55
  111. package/dist/server/routes/graph-api.js.map +1 -1
  112. package/dist/server/routes/mcp.d.ts +12 -0
  113. package/dist/server/routes/mcp.js +35 -0
  114. package/dist/server/routes/mcp.js.map +1 -0
  115. package/dist/server/routes/permissions-api.d.ts +2 -1
  116. package/dist/server/routes/permissions-api.js +16 -2
  117. package/dist/server/routes/permissions-api.js.map +1 -1
  118. package/dist/server/routes/sync-api.d.ts +26 -0
  119. package/dist/server/routes/sync-api.js +757 -0
  120. package/dist/server/routes/sync-api.js.map +1 -0
  121. package/dist/server/routes/ws.d.ts +4 -2
  122. package/dist/server/routes/ws.js +100 -4
  123. package/dist/server/routes/ws.js.map +1 -1
  124. package/dist/server/session-manager.d.ts +40 -0
  125. package/dist/server/session-manager.js +132 -0
  126. package/dist/server/session-manager.js.map +1 -0
  127. package/dist/server/ws-hub.d.ts +95 -1
  128. package/dist/server/ws-hub.js +192 -5
  129. package/dist/server/ws-hub.js.map +1 -1
  130. package/dist/server/yjs-manager.d.ts +59 -0
  131. package/dist/server/yjs-manager.js +194 -0
  132. package/dist/server/yjs-manager.js.map +1 -0
  133. package/dist/services/auth-service.d.ts +30 -2
  134. package/dist/services/auth-service.js +116 -11
  135. package/dist/services/auth-service.js.map +1 -1
  136. package/dist/services/git.d.ts +6 -0
  137. package/dist/services/git.js +32 -2
  138. package/dist/services/git.js.map +1 -1
  139. package/dist/services/sync/collection-sync.d.ts +73 -0
  140. package/dist/services/sync/collection-sync.js +726 -0
  141. package/dist/services/sync/collection-sync.js.map +1 -0
  142. package/dist/services/sync/commit.js +5 -20
  143. package/dist/services/sync/commit.js.map +1 -1
  144. package/dist/services/sync/data-fetcher.d.ts +31 -0
  145. package/dist/services/sync/data-fetcher.js +12 -0
  146. package/dist/services/sync/data-fetcher.js.map +1 -0
  147. package/dist/services/sync/entity-refresh.d.ts +30 -0
  148. package/dist/services/sync/entity-refresh.js +275 -0
  149. package/dist/services/sync/entity-refresh.js.map +1 -0
  150. package/dist/services/sync/frontmatter-extractor.d.ts +2 -2
  151. package/dist/services/sync/frontmatter-extractor.js +1 -2
  152. package/dist/services/sync/frontmatter-extractor.js.map +1 -1
  153. package/dist/services/sync/graph-match.js +1 -1
  154. package/dist/services/sync/graph-match.js.map +1 -1
  155. package/dist/services/sync/mcp-client.d.ts +16 -4
  156. package/dist/services/sync/mcp-client.js +34 -20
  157. package/dist/services/sync/mcp-client.js.map +1 -1
  158. package/dist/services/sync/prompts.js +1 -1
  159. package/dist/services/sync/reconciler.js +1 -2
  160. package/dist/services/sync/reconciler.js.map +1 -1
  161. package/dist/services/sync/rest-client.d.ts +40 -0
  162. package/dist/services/sync/rest-client.js +100 -0
  163. package/dist/services/sync/rest-client.js.map +1 -0
  164. package/dist/services/sync/source-config.d.ts +23 -1
  165. package/dist/services/sync/source-config.js +112 -16
  166. package/dist/services/sync/source-config.js.map +1 -1
  167. package/dist/services/sync/source-definitions/asana.d.ts +3 -4
  168. package/dist/services/sync/source-definitions/asana.js +7 -13
  169. package/dist/services/sync/source-definitions/asana.js.map +1 -1
  170. package/dist/services/sync/source-definitions/definitions.d.ts +5 -18
  171. package/dist/services/sync/source-definitions/definitions.js +4 -22
  172. package/dist/services/sync/source-definitions/definitions.js.map +1 -1
  173. package/dist/services/sync/source-definitions/granola.d.ts +1 -1
  174. package/dist/services/sync/source-definitions/granola.js +11 -18
  175. package/dist/services/sync/source-definitions/granola.js.map +1 -1
  176. package/dist/services/sync/source-definitions/linear.d.ts +1 -1
  177. package/dist/services/sync/source-definitions/linear.js +17 -15
  178. package/dist/services/sync/source-definitions/linear.js.map +1 -1
  179. package/dist/services/sync/source-definitions/pipedrive.d.ts +1 -1
  180. package/dist/services/sync/source-definitions/pipedrive.js +6 -15
  181. package/dist/services/sync/source-definitions/pipedrive.js.map +1 -1
  182. package/dist/services/sync/staging.js +1 -2
  183. package/dist/services/sync/staging.js.map +1 -1
  184. package/dist/services/sync/structured-extractor.d.ts +8 -2
  185. package/dist/services/sync/structured-extractor.js +243 -35
  186. package/dist/services/sync/structured-extractor.js.map +1 -1
  187. package/dist/services/sync/types.d.ts +192 -23
  188. package/dist/services/sync/unstructured-extractor.d.ts +1 -3
  189. package/dist/services/sync/unstructured-extractor.js +2 -14
  190. package/dist/services/sync/unstructured-extractor.js.map +1 -1
  191. package/dist/utils/git.d.ts +24 -20
  192. package/dist/utils/git.js +99 -65
  193. package/dist/utils/git.js.map +1 -1
  194. package/dist/utils/preflight.d.ts +1 -15
  195. package/dist/utils/preflight.js +1 -35
  196. package/dist/utils/preflight.js.map +1 -1
  197. package/dist/web/_app/immutable/assets/0.CupILLQs.css +1 -0
  198. package/dist/web/_app/immutable/assets/3.CtJi4Cy9.css +1 -0
  199. package/dist/web/_app/immutable/assets/5.CydFyZSu.css +1 -0
  200. package/dist/web/_app/immutable/assets/6.kqeOo0OW.css +1 -0
  201. package/dist/web/_app/immutable/assets/7.CseIx7qQ.css +1 -0
  202. package/dist/web/_app/immutable/assets/{8.Sm6jB3a0.css → 8.BYpFDZHK.css} +1 -1
  203. package/dist/web/_app/immutable/assets/AppShell.Ch_ef9hJ.css +1 -0
  204. package/dist/web/_app/immutable/assets/ChatPanel.CP-_8txt.css +1 -0
  205. package/dist/web/_app/immutable/chunks/0oxpWEgM.js +1 -0
  206. package/dist/web/_app/immutable/chunks/B1y7Wy5O.js +18 -0
  207. package/dist/web/_app/immutable/chunks/B7eduG_j.js +64 -0
  208. package/dist/web/_app/immutable/chunks/BBLgaWN8.js +1 -0
  209. package/dist/web/_app/immutable/chunks/BCB5cYCz.js +2 -0
  210. package/dist/web/_app/immutable/chunks/{aosHekRC.js → BPUy9_sS.js} +1 -1
  211. package/dist/web/_app/immutable/chunks/BVBRzmeQ.js +7 -0
  212. package/dist/web/_app/immutable/chunks/{CUzqHQY_.js → BXuvR8Ks.js} +2 -1
  213. package/dist/web/_app/immutable/chunks/BeBar3OL.js +1 -0
  214. package/dist/web/_app/immutable/chunks/BuOTIbJu.js +1 -0
  215. package/dist/web/_app/immutable/chunks/CLFba8FK.js +5 -0
  216. package/dist/web/_app/immutable/chunks/CQCkXCml.js +1 -0
  217. package/dist/web/_app/immutable/chunks/CXuhHL4d.js +1 -0
  218. package/dist/web/_app/immutable/chunks/Cg9NOuOl.js +27 -0
  219. package/dist/web/_app/immutable/chunks/Cs5oz2oJ.js +5 -0
  220. package/dist/web/_app/immutable/chunks/Cs_ROD7H.js +2 -0
  221. package/dist/web/_app/immutable/chunks/D2aTbzFm.js +3 -0
  222. package/dist/web/_app/immutable/chunks/D4FXhiC2.js +1 -0
  223. package/dist/web/_app/immutable/chunks/D4VHRYeB.js +1 -0
  224. package/dist/web/_app/immutable/chunks/DCGSm8Hl.js +1 -0
  225. package/dist/web/_app/immutable/chunks/DP09rP34.js +2 -0
  226. package/dist/web/_app/immutable/chunks/DiP47fAp.js +1 -0
  227. package/dist/web/_app/immutable/chunks/DptGlK8O.js +1 -0
  228. package/dist/web/_app/immutable/chunks/O0fx2ss6.js +1 -0
  229. package/dist/web/_app/immutable/chunks/xBRYfpah.js +1 -0
  230. package/dist/web/_app/immutable/entry/app.FgnywZP_.js +2 -0
  231. package/dist/web/_app/immutable/entry/start.Bsa-zlPf.js +1 -0
  232. package/dist/web/_app/immutable/nodes/0.D3SW-LMc.js +10 -0
  233. package/dist/web/_app/immutable/nodes/1.y0c5TQTP.js +1 -0
  234. package/dist/web/_app/immutable/nodes/2.BQfSep9-.js +1 -0
  235. package/dist/web/_app/immutable/nodes/3.CC4Y-xMM.js +11 -0
  236. package/dist/web/_app/immutable/nodes/4.Dp0Z-oPW.js +4 -0
  237. package/dist/web/_app/immutable/nodes/5.gjZ03DON.js +2 -0
  238. package/dist/web/_app/immutable/nodes/6.dRNIwcJQ.js +1 -0
  239. package/dist/web/_app/immutable/nodes/7.I4Gjes3o.js +2 -0
  240. package/dist/web/_app/immutable/nodes/8.Dj14D7uH.js +1 -0
  241. package/dist/web/_app/version.json +1 -1
  242. package/dist/web/index.html +10 -12
  243. package/package.json +4 -2
  244. package/dist/web/_app/immutable/assets/0.CF0XhAap.css +0 -1
  245. package/dist/web/_app/immutable/assets/3.BJy7pVXi.css +0 -1
  246. package/dist/web/_app/immutable/assets/4.Ad16uh9o.css +0 -1
  247. package/dist/web/_app/immutable/assets/6.Bm2i7O0j.css +0 -1
  248. package/dist/web/_app/immutable/assets/7.Cn2DG-J6.css +0 -1
  249. package/dist/web/_app/immutable/assets/AppShell.CztjTuKY.css +0 -1
  250. package/dist/web/_app/immutable/assets/ChatPanel.RFD5GGYI.css +0 -1
  251. package/dist/web/_app/immutable/assets/editor.CPAf2SRV.css +0 -1
  252. package/dist/web/_app/immutable/chunks/4QY4j-jX.js +0 -1
  253. package/dist/web/_app/immutable/chunks/B3Kdf1r4.js +0 -6
  254. package/dist/web/_app/immutable/chunks/BIo3H1KR.js +0 -2
  255. package/dist/web/_app/immutable/chunks/BJLM1w2L.js +0 -23
  256. package/dist/web/_app/immutable/chunks/BLCwEMdm.js +0 -1
  257. package/dist/web/_app/immutable/chunks/BSYvCVJt.js +0 -1
  258. package/dist/web/_app/immutable/chunks/Ba5JX1o9.js +0 -1
  259. package/dist/web/_app/immutable/chunks/Bj34y868.js +0 -64
  260. package/dist/web/_app/immutable/chunks/BrWpHgBJ.js +0 -1
  261. package/dist/web/_app/immutable/chunks/C3g_lwol.js +0 -1
  262. package/dist/web/_app/immutable/chunks/CXnPm09s.js +0 -1
  263. package/dist/web/_app/immutable/chunks/ClOTom10.js +0 -1
  264. package/dist/web/_app/immutable/chunks/CtT4aw_G.js +0 -1
  265. package/dist/web/_app/immutable/chunks/Dh_H7Owr.js +0 -18
  266. package/dist/web/_app/immutable/chunks/DnlgZ_Tk.js +0 -5
  267. package/dist/web/_app/immutable/chunks/DtVH--hH.js +0 -6
  268. package/dist/web/_app/immutable/chunks/Dzd9kdLj.js +0 -2
  269. package/dist/web/_app/immutable/chunks/L91a_BGe.js +0 -1
  270. package/dist/web/_app/immutable/chunks/bHAllEMt.js +0 -1
  271. package/dist/web/_app/immutable/entry/app.CdrgaaFb.js +0 -2
  272. package/dist/web/_app/immutable/entry/start.t9LMjt48.js +0 -1
  273. package/dist/web/_app/immutable/nodes/0.P-Xfebn4.js +0 -2
  274. package/dist/web/_app/immutable/nodes/1.DiZlq1e6.js +0 -1
  275. package/dist/web/_app/immutable/nodes/2.ByIZ5J2p.js +0 -1
  276. package/dist/web/_app/immutable/nodes/3.D7JhktsZ.js +0 -1
  277. package/dist/web/_app/immutable/nodes/4.BtMeWbPx.js +0 -16
  278. package/dist/web/_app/immutable/nodes/5.CCYG1pbQ.js +0 -4
  279. package/dist/web/_app/immutable/nodes/6.CL_Ah04j.js +0 -2
  280. package/dist/web/_app/immutable/nodes/7.BMTaosAj.js +0 -1
  281. package/dist/web/_app/immutable/nodes/8.mrPg67cz.js +0 -1
  282. /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 { gitPull, gitPush, gitStashPop, gitHasRemote } from '../../utils/git.js';
16
- import { resolveConflicts } from '../../utils/merge-resolver.js';
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 per-file resolution summaries after conflict resolution.
40
+ * Print manual resolution guidance when pull has diverged.
40
41
  */
41
- function logResolutions(entryName, resolution) {
42
- if (resolution.resolutions.length === 0)
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
- const resolvedCount = resolution.resolutions.filter(r => r.resolved).length;
45
- if (resolution.success) {
46
- log.success(`${entryName}: updated (${resolvedCount} conflict${resolvedCount === 1 ? '' : 's'} resolved)`);
47
- for (const r of resolution.resolutions) {
48
- if (r.resolved) {
49
- log.info(` ${r.file} — ${r.summary}`);
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
- else {
54
- const failed = resolution.resolutions.filter(r => !r.resolved);
55
- log.warn(`${entryName}: ${failed.length} conflict${failed.length === 1 ? '' : 's'} could not be resolved`);
56
- for (const r of resolution.resolutions) {
57
- if (!r.resolved) {
58
- log.info(` ${r.file} ${r.summary ?? 'unresolved'}`);
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
- else {
61
- log.info(` ${r.file} ${r.summary} (resolved, but rebase aborted)`);
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
- * Print manual resolution guidance for a failed conflict.
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
- .action(async () => {
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
- for (const entry of entries) {
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(entry.path, { abortOnConflict: false });
199
+ const result = gitPull(configPath);
97
200
  if (result === 'updated') {
98
- log.success(`${entry.name}: updated`);
201
+ log.success('.studiograph: updated');
99
202
  }
100
203
  else if (result === 'up-to-date') {
101
- log.info(`${entry.name}: already up to date`);
204
+ log.info('.studiograph: already up to date');
102
205
  }
103
- else {
104
- // Conflict — attempt resolution
105
- const resolution = await resolveConflicts(entry.path, (msg) => log.info(msg));
106
- gitStashPop(entry.path); // restore any auto-stashed local changes
107
- if (resolution.success) {
108
- logResolutions(entry.name, resolution);
109
- }
110
- else if (resolution.resolutions.length > 0) {
111
- logResolutions(entry.name, resolution);
112
- logManualGuidance(entry.path);
113
- hasConflict = true;
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
- else {
116
- log.warn(`${entry.name}: conflict resolve manually in ${entry.path}`);
117
- logManualGuidance(entry.path);
118
- hasConflict = true;
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(`${entry.name}: ${error instanceof Error ? error.message : 'Unknown 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
- .action(async () => {
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.info(`${entry.name}: remote has newer commits, pulling...`);
159
- const pullResult = gitPull(entry.path, { abortOnConflict: false });
160
- if (pullResult === 'conflict') {
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('Push failed for one or more collections.');
628
+ outro('Push failed for one or more collections.');
193
629
  process.exit(1);
194
630
  }
195
631
  });