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