synsci 1.2.4 → 1.2.6

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 (2) hide show
  1. package/bin/synsci.mjs +190 -57
  2. package/package.json +2 -2
package/bin/synsci.mjs CHANGED
@@ -25,7 +25,11 @@ import { createInterface } from "node:readline"
25
25
  import { fileURLToPath } from "node:url"
26
26
 
27
27
  const SELF_PATH = (() => {
28
- try { return realpathSync(fileURLToPath(import.meta.url)) } catch { return "" }
28
+ try {
29
+ return realpathSync(fileURLToPath(import.meta.url))
30
+ } catch {
31
+ return ""
32
+ }
29
33
  })()
30
34
 
31
35
  const BOLD = "\x1b[1m"
@@ -40,22 +44,26 @@ const SHOW_CURSOR = "\x1b[?25h"
40
44
  const CLEAR_LINE = "\x1b[2K\r"
41
45
 
42
46
  const LOGO = [
43
- " ██████╗ ██████╗ ███████╗███╗ ██╗",
44
- "██╔═══██╗██╔══██╗██╔════╝████╗ ██║",
45
- "██║ ██║██████╔╝█████╗ ██╔██╗ ██║",
46
- "██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║",
47
- "╚██████╔╝██║ ███████╗██║ ╚████║",
48
- " ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝",
49
- "███████╗ ██████╗██╗███████╗███╗ ██╗ ██████╗███████╗",
50
- "██╔════╝██╔════╝██║██╔════╝████╗ ██║██╔════╝██╔════╝",
51
- "███████╗██║ ██║█████╗ ██╔██╗ ██║██║ █████╗ ",
52
- "╚════██║██║ ██║██╔══╝ ██║╚██╗██║██║ ██╔══╝ ",
53
- "███████║╚██████╗██║███████╗██║ ╚████║╚██████╗███████╗",
54
- "╚══════╝ ╚═════╝╚═╝╚══════╝╚═╝ ╚═══╝ ╚═════╝╚══════╝",
47
+ "███████╗██╗ ██╗███╗ ██╗████████╗██╗ ██╗███████╗████████╗██╗ ██████╗",
48
+ "██╔════╝╚██╗ ██╔╝████╗ ██║╚══██╔══╝██║ ██║██╔════╝╚══██╔══╝██║██╔════╝",
49
+ "███████╗ ╚████╔╝ ██╔██╗ ██║ ██║ ███████║█████╗ ██║ ██║██║ ",
50
+ "╚════██║ ╚██╔╝ ██║╚██╗██║ ██║ ██╔══██║██╔══╝ ██║ ██║██║ ",
51
+ "███████║ ██║ ██║ ╚████║ ██║ ██║ ██║███████╗ ██║ ██║╚██████╗",
52
+ "╚══════╝ ╚═╝ ╚═╝ ╚═══╝ ╚═╝ ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═════╝",
53
+ "███████╗ ██████╗██╗███████╗███╗ ██╗ ██████╗███████╗███████╗",
54
+ "██╔════╝██╔════╝██║██╔════╝████╗ ██║██╔════╝██╔════╝██╔════╝",
55
+ "███████╗██║ ██║█████╗ ██╔██╗ ██║██║ █████╗ ███████╗",
56
+ "╚════██║██║ ██║██╔══╝ ██║╚██╗██║██║ ██╔══╝ ╚════██║",
57
+ "███████║╚██████╗██║███████╗██║ ╚████║╚██████╗███████╗███████║",
58
+ "╚══════╝ ╚═════╝╚═╝╚══════╝╚═╝ ╚═══╝ ╚═════╝╚══════╝╚══════╝",
55
59
  ]
56
60
 
57
- function ok(msg) { console.log(` ${GREEN}✓${RESET} ${msg}`) }
58
- function warn(msg) { console.log(` ${YELLOW}⚠${RESET} ${msg}`) }
61
+ function ok(msg) {
62
+ console.log(` ${GREEN}✓${RESET} ${msg}`)
63
+ }
64
+ function warn(msg) {
65
+ console.log(` ${YELLOW}⚠${RESET} ${msg}`)
66
+ }
59
67
 
60
68
  function spinner(msg) {
61
69
  const frames = ["◒", "◐", "◓", "◑"]
@@ -65,21 +73,50 @@ function spinner(msg) {
65
73
  process.stdout.write(`${CLEAR_LINE} ${CYAN}${frames[i++ % frames.length]}${RESET} ${msg}`)
66
74
  }, 80)
67
75
  return {
68
- ok(result) { clearInterval(id); process.stdout.write(`${CLEAR_LINE}${SHOW_CURSOR}`); ok(result) },
69
- warn(result) { clearInterval(id); process.stdout.write(`${CLEAR_LINE}${SHOW_CURSOR}`); warn(result) },
70
- fail(result) { clearInterval(id); process.stdout.write(`${CLEAR_LINE}${SHOW_CURSOR}`); console.log(` ${RED}✗${RESET} ${result}`) },
71
- update(m) { msg = m },
76
+ ok(result) {
77
+ clearInterval(id)
78
+ process.stdout.write(`${CLEAR_LINE}${SHOW_CURSOR}`)
79
+ ok(result)
80
+ },
81
+ warn(result) {
82
+ clearInterval(id)
83
+ process.stdout.write(`${CLEAR_LINE}${SHOW_CURSOR}`)
84
+ warn(result)
85
+ },
86
+ fail(result) {
87
+ clearInterval(id)
88
+ process.stdout.write(`${CLEAR_LINE}${SHOW_CURSOR}`)
89
+ console.log(` ${RED}✗${RESET} ${result}`)
90
+ },
91
+ update(m) {
92
+ msg = m
93
+ },
72
94
  }
73
95
  }
74
96
 
75
97
  function runQuiet(cmd) {
76
- try { return execSync(cmd, { encoding: "utf-8", stdio: "pipe" }).trim() }
77
- catch { return null }
98
+ try {
99
+ return execSync(cmd, { encoding: "utf-8", stdio: "pipe" }).trim()
100
+ } catch {
101
+ return null
102
+ }
103
+ }
104
+
105
+ // Windows global installs expose a .cmd shim, which can't be exec'd
106
+ // directly — it needs a shell (and the path quoted for it).
107
+ const isCmdShim = (p) => process.platform === "win32" && p.toLowerCase().endsWith(".cmd")
108
+
109
+ function execCli(file, args = [], opts = {}) {
110
+ if (isCmdShim(file)) return execSync(['"' + file + '"', ...args].join(" "), opts)
111
+ return execFileSync(file, args, opts)
78
112
  }
79
113
 
80
114
  function runFileQuiet(file, args = []) {
81
- try { return execFileSync(file, args, { encoding: "utf-8", stdio: "pipe" }).trim() }
82
- catch { return null }
115
+ try {
116
+ return execCli(file, args, { encoding: "utf-8", stdio: "pipe" }).trim()
117
+ } catch {
118
+ return null
119
+ }
83
120
  }
84
121
 
85
122
  function isLauncherPath(p) {
@@ -88,7 +125,9 @@ function isLauncherPath(p) {
88
125
  if (SELF_PATH && real === SELF_PATH) return true
89
126
  if (real.includes("/_npx/")) return true
90
127
  return false
91
- } catch { return false }
128
+ } catch {
129
+ return false
130
+ }
92
131
  }
93
132
 
94
133
  // Returns the absolute path to the real @synsci/openscience binary (`openscience`).
@@ -97,20 +136,29 @@ function isLauncherPath(p) {
97
136
  // `--version` so half-broken installs are skipped instead of accepted.
98
137
  function resolveCli() {
99
138
  const candidates = []
100
- // 1. Global npm prefix (where `npm i -g @synsci/openscience` puts it)
139
+ // 1. Global npm prefix (where `npm i -g @synsci/openscience` puts it).
140
+ // On Windows the global bin dir is the prefix itself and the entry is an
141
+ // openscience.cmd shim; on POSIX it's <prefix>/bin/openscience.
101
142
  const prefix = runQuiet("npm prefix -g")
102
- if (prefix) candidates.push(join(prefix, "bin", "openscience"))
103
- // 2. ~/.openscience/bin/openscience (curl-installer location)
104
- candidates.push(join(homedir(), ".openscience", "bin", "openscience"))
143
+ if (prefix) {
144
+ if (process.platform === "win32") candidates.push(join(prefix, "openscience.cmd"))
145
+ else candidates.push(join(prefix, "bin", "openscience"))
146
+ }
147
+ // 2. ~/.openscience/bin/openscience (curl-installer location, POSIX only)
148
+ if (process.platform !== "win32") candidates.push(join(homedir(), ".openscience", "bin", "openscience"))
105
149
 
106
150
  for (const cand of candidates) {
107
151
  if (!existsSync(cand) || isLauncherPath(cand)) continue
108
152
  try {
109
- const ver = execFileSync(cand, ["--version"], {
110
- encoding: "utf-8", stdio: "pipe", timeout: 5000,
153
+ const ver = execCli(cand, ["--version"], {
154
+ encoding: "utf-8",
155
+ stdio: "pipe",
156
+ timeout: 5000,
111
157
  }).trim()
112
158
  if (/^\d/.test(ver)) return cand
113
- } catch { /* unrunnable candidate, try next */ }
159
+ } catch {
160
+ /* unrunnable candidate, try next */
161
+ }
114
162
  }
115
163
  return null
116
164
  }
@@ -122,10 +170,16 @@ function resolveCli() {
122
170
  // still prints JSON, so read stdout either way.
123
171
  function hasDeprecatedCli() {
124
172
  let out = ""
125
- try { out = execSync("npm ls -g @synsci/cli --depth=0 --json", { encoding: "utf-8", stdio: "pipe" }) }
126
- catch (e) { out = e && typeof e.stdout === "string" ? e.stdout : "" }
127
- try { return Boolean(JSON.parse(out).dependencies["@synsci/cli"]) }
128
- catch { return false }
173
+ try {
174
+ out = execSync("npm ls -g @synsci/cli --depth=0 --json", { encoding: "utf-8", stdio: "pipe" })
175
+ } catch (e) {
176
+ out = e && typeof e.stdout === "string" ? e.stdout : ""
177
+ }
178
+ try {
179
+ return Boolean(JSON.parse(out).dependencies["@synsci/cli"])
180
+ } catch {
181
+ return false
182
+ }
129
183
  }
130
184
 
131
185
  function isConnected() {
@@ -136,29 +190,73 @@ function isConnected() {
136
190
  const data = JSON.parse(readFileSync(sessionPath, "utf-8"))
137
191
  if (!data.access_token || !data.expires_at) return false
138
192
  return new Date(data.expires_at) > new Date()
139
- } catch { return false }
193
+ } catch {
194
+ return false
195
+ }
196
+ }
197
+
198
+ function atlasVersion() {
199
+ // `atlas` on PATH may be Ariga Atlas or the MongoDB Atlas CLI, whose
200
+ // --version output also survives the digit filter. Only trust the command
201
+ // when the global @synsci/atlas package is present to own it.
202
+ const owned = runQuiet("npm ls -g @synsci/atlas --depth=0")
203
+ if (!owned || !owned.includes("@synsci/atlas")) return null
204
+ const raw = runQuiet("atlas --version")
205
+ return raw ? raw.replace(/[^0-9.]/g, "") : null
140
206
  }
141
207
 
142
- function hasAtlas() {
143
- return runQuiet("atlas --version") !== null
208
+ // Install the Atlas CLI, or bring an existing install up to date. Returns
209
+ // true when a working `atlas` binary is on PATH afterwards.
210
+ async function installOrUpdateAtlas() {
211
+ const current = atlasVersion()
212
+ if (current) {
213
+ const s = spinner("Checking Atlas CLI for updates...")
214
+ const latest = runQuiet("npm view @synsci/atlas version")
215
+ if (!latest || current === latest) {
216
+ s.ok(`atlas ${current} ${DIM}(up to date)${RESET}`)
217
+ return true
218
+ }
219
+ s.update(`Upgrading Atlas CLI ${current} → ${latest}...`)
220
+ if (runQuiet("npm i -g @synsci/atlas@latest") !== null) {
221
+ s.ok(`Upgraded Atlas CLI to ${latest}`)
222
+ } else {
223
+ s.warn(`Atlas CLI upgrade failed, continuing with ${current}`)
224
+ }
225
+ return true
226
+ }
227
+ const s = spinner("Installing Atlas CLI...")
228
+ if (runQuiet("npm i -g @synsci/atlas@latest") !== null && atlasVersion()) {
229
+ s.ok("Installed Atlas CLI")
230
+ return true
231
+ }
232
+ s.warn(`Atlas CLI install failed, you can retry later: ${CYAN}npm i -g @synsci/atlas${RESET}`)
233
+ return false
144
234
  }
145
235
 
146
236
  async function ask(question) {
147
237
  const rl = createInterface({ input: process.stdin, output: process.stdout })
148
238
  return new Promise((resolve) => {
149
- rl.question(question, (answer) => { rl.close(); resolve(answer.trim()) })
239
+ rl.question(question, (answer) => {
240
+ rl.close()
241
+ resolve(answer.trim())
242
+ })
150
243
  })
151
244
  }
152
245
 
153
246
  async function main() {
154
247
  process.on("exit", () => process.stdout.write(SHOW_CURSOR))
155
- process.on("SIGINT", () => { process.stdout.write(SHOW_CURSOR); process.exit(130) })
248
+ process.on("SIGINT", () => {
249
+ process.stdout.write(SHOW_CURSOR)
250
+ process.exit(130)
251
+ })
156
252
 
157
253
  // --- Logo ---
158
254
  console.log()
159
255
  for (const line of LOGO) console.log(` ${CYAN}${line}${RESET}`)
160
256
  console.log()
161
- console.log(` ${BOLD}OpenScience${RESET} ${DIM}the open-source AI research workspace${RESET}`)
257
+ console.log(
258
+ ` ${BOLD}Synthetic Sciences${RESET} ${DIM}OpenScience, the open-source AI research workspace · Atlas, the research platform${RESET}`,
259
+ )
162
260
  console.log()
163
261
 
164
262
  // --- Step 1: Install or upgrade the OpenScience CLI ---
@@ -186,7 +284,7 @@ async function main() {
186
284
  } else {
187
285
  s.update(`Upgrading ${current} → ${latest}...`)
188
286
  try {
189
- execFileSync(cliPath, ["upgrade"], { stdio: "pipe" })
287
+ execCli(cliPath, ["upgrade"], { stdio: "pipe" })
190
288
  s.ok(`Upgraded to ${latest}`)
191
289
  } catch {
192
290
  s.warn(`Upgrade failed, continuing with ${current}`)
@@ -214,6 +312,13 @@ async function main() {
214
312
  if (!cliPath) throw new Error("openscience not on PATH after install")
215
313
  s.ok("Installed OpenScience")
216
314
  } catch {
315
+ // The standalone installer is a bash script; on native Windows there's
316
+ // no bash to pipe it into, so don't suggest a fallback that can't run.
317
+ if (process.platform === "win32") {
318
+ s.fail("Install failed")
319
+ console.log(`\n Try manually: ${CYAN}npm i -g @synsci/openscience${RESET}\n`)
320
+ process.exit(1)
321
+ }
217
322
  // Global npm installs commonly fail on permissions. Fall back to the
218
323
  // standalone installer, which lands in ~/.openscience/bin without sudo
219
324
  // (resolveCli already checks that location).
@@ -236,31 +341,56 @@ async function main() {
236
341
  console.log()
237
342
  console.log(` ${BOLD}How do you want to run it?${RESET}`)
238
343
  console.log()
239
- console.log(` ${BOLD}1${RESET} ${CYAN}OpenScience${RESET} ${DIM}free and open source, bring your own API keys, no account${RESET}`)
240
- console.log(` ${BOLD}2${RESET} ${CYAN}OpenScience + Atlas${RESET} ${DIM}managed models, wallet billing, research graph & compute${RESET}`)
344
+ console.log(
345
+ ` ${BOLD}1${RESET} ${CYAN}OpenScience${RESET} ${DIM}free and open source, bring your own API keys, no account${RESET}`,
346
+ )
347
+ console.log(
348
+ ` ${BOLD}2${RESET} ${CYAN}OpenScience + Atlas${RESET} ${DIM}managed models, wallet billing, research graph & compute${RESET}`,
349
+ )
350
+ console.log(
351
+ ` ${BOLD}3${RESET} ${CYAN}Atlas CLI${RESET} ${DIM}just the Atlas research CLI — maps, runs, and compute from the terminal${RESET}`,
352
+ )
241
353
  console.log()
242
354
 
243
- const setup = await ask(` ${DIM}❯${RESET} Choose [1/2]: `)
355
+ const setup = await ask(` ${DIM}❯${RESET} Choose [1/2/3]: `)
244
356
  console.log()
245
357
 
246
- if (setup === "2") {
247
- if (!hasAtlas()) {
248
- const s = spinner("Installing Atlas CLI...")
249
- try {
250
- execSync("npm i -g @synsci/atlas@latest", { stdio: "pipe" })
251
- s.ok("Installed Atlas CLI")
252
- } catch {
253
- s.warn(`Atlas CLI install failed, you can retry later: ${CYAN}npm i -g @synsci/atlas${RESET}`)
254
- }
358
+ if (setup === "3") {
359
+ // Atlas-CLI-only path: install/upgrade, verify, hand off to `atlas`.
360
+ // No workspace launch this option exists for people who want the
361
+ // research CLI on a box without opening the browser app.
362
+ const installed = await installOrUpdateAtlas()
363
+ if (!installed) process.exit(1)
364
+ const s = spinner("Running atlas doctor...")
365
+ if (runQuiet("atlas doctor") !== null) {
366
+ s.ok("atlas doctor passed")
367
+ } else {
368
+ s.warn(`atlas doctor reported issues — run ${CYAN}atlas doctor${RESET} for details`)
369
+ }
370
+ if (isConnected()) {
371
+ ok("Connected to Atlas")
255
372
  } else {
256
- ok("Atlas CLI already installed")
373
+ console.log()
374
+ console.log(
375
+ ` ${DIM}Connect your Atlas account for managed credentials:${RESET} ${CYAN}openscience connect login${RESET}`,
376
+ )
257
377
  }
378
+ console.log()
379
+ console.log(` ${BOLD}Next steps${RESET}`)
380
+ console.log(` ${CYAN}atlas --help${RESET} ${DIM}see everything the CLI can do${RESET}`)
381
+ console.log(` ${CYAN}atlas doctor${RESET} ${DIM}check credentials and environment${RESET}`)
382
+ console.log()
383
+ process.exit(0)
384
+ }
385
+
386
+ if (setup === "2") {
387
+ await installOrUpdateAtlas()
258
388
  if (isConnected()) {
259
389
  ok("Connected to Atlas")
260
390
  } else {
261
391
  console.log()
262
392
  try {
263
- execFileSync(cliPath, ["connect", "login"], { stdio: "inherit" })
393
+ execCli(cliPath, ["connect", "login"], { stdio: "inherit" })
264
394
  } catch {}
265
395
  }
266
396
  } else {
@@ -273,7 +403,10 @@ async function main() {
273
403
  console.log(` ${DIM}Opening the workspace in your browser…${RESET}`)
274
404
  console.log()
275
405
 
276
- const child = spawn(cliPath, ["web", ...process.argv.slice(2)], { stdio: "inherit" })
406
+ const webArgs = ["web", ...process.argv.slice(2)]
407
+ const child = isCmdShim(cliPath)
408
+ ? spawn(['"' + cliPath + '"', ...webArgs].join(" "), { stdio: "inherit", shell: true })
409
+ : spawn(cliPath, webArgs, { stdio: "inherit" })
277
410
  child.on("close", (code) => process.exit(code ?? 0))
278
411
  }
279
412
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synsci",
3
- "version": "1.2.4",
3
+ "version": "1.2.6",
4
4
  "description": "Install wizard for OpenScience, the open-source AI research workspace (optionally with Atlas)",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -25,4 +25,4 @@
25
25
  "url": "git+https://github.com/synthetic-sciences/openscience.git"
26
26
  },
27
27
  "homepage": "https://syntheticsciences.ai"
28
- }
28
+ }