@joystack/cli 0.0.0-stage → 0.1.44

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 (56) hide show
  1. package/CHANGELOG.md +749 -0
  2. package/LICENSE +202 -0
  3. package/README.md +21 -2
  4. package/bin/joystack.mjs +300 -0
  5. package/lib/agent-session.mjs +52 -0
  6. package/lib/agent-surface.mjs +333 -0
  7. package/lib/assets-json.mjs +205 -0
  8. package/lib/assets-page.mjs +184 -0
  9. package/lib/assets-sheet.mjs +365 -0
  10. package/lib/build-id.mjs +130 -0
  11. package/lib/bundle-primitives.mjs +123 -0
  12. package/lib/changelog.mjs +193 -0
  13. package/lib/config.mjs +56 -0
  14. package/lib/create.mjs +243 -0
  15. package/lib/dev.mjs +435 -0
  16. package/lib/engine.mjs +198 -0
  17. package/lib/feedback-sync.mjs +231 -0
  18. package/lib/feedback.mjs +209 -0
  19. package/lib/freeze.mjs +108 -0
  20. package/lib/game-files.mjs +99 -0
  21. package/lib/history.mjs +612 -0
  22. package/lib/inbox-cmd.mjs +57 -0
  23. package/lib/inject.mjs +250 -0
  24. package/lib/joystack-node-doors.mjs +230 -0
  25. package/lib/lane.mjs +141 -0
  26. package/lib/legacy-names.mjs +275 -0
  27. package/lib/lint.mjs +292 -0
  28. package/lib/login.mjs +134 -0
  29. package/lib/model-bake-cli.mjs +17 -0
  30. package/lib/model-bake.mjs +869 -0
  31. package/lib/npm.mjs +38 -0
  32. package/lib/ports.mjs +234 -0
  33. package/lib/primitives.mjs +393 -0
  34. package/lib/printable.mjs +12 -0
  35. package/lib/process-tree.mjs +30 -0
  36. package/lib/project.mjs +84 -0
  37. package/lib/publish.mjs +179 -0
  38. package/lib/replay-cmd.mjs +426 -0
  39. package/lib/replay-export.mjs +1379 -0
  40. package/lib/replay-list.mjs +308 -0
  41. package/lib/replay-profile.mjs +467 -0
  42. package/lib/replay-store.mjs +719 -0
  43. package/lib/room-reload.mjs +168 -0
  44. package/lib/room-server.mjs +539 -0
  45. package/lib/self-update.mjs +162 -0
  46. package/lib/shadow-resolve.mjs +85 -0
  47. package/lib/share.mjs +149 -0
  48. package/lib/smoke-gpu-preload.mjs +91 -0
  49. package/lib/source-path.mjs +33 -0
  50. package/lib/static-server.mjs +1238 -0
  51. package/lib/test-cmd.mjs +697 -0
  52. package/lib/test-policy.mjs +157 -0
  53. package/lib/test-reporter.mjs +177 -0
  54. package/lib/test-selection-cli.mjs +18 -0
  55. package/lib/update.mjs +316 -0
  56. package/package.json +27 -4
package/lib/login.mjs ADDED
@@ -0,0 +1,134 @@
1
+ // `joystack login` — device-flow authentication (the `gh auth login` shape):
2
+ //
3
+ // 1. ask the platform for a pairing (device_code for us, user_code for the
4
+ // human),
5
+ // 2. show the human the short code and open /activate in their browser,
6
+ // 3. poll until they approve,
7
+ // 4. store the minted creator token in ~/.joystack/config.json.
8
+ //
9
+ // The secret never passes through the human's hands or the chat — non-dev
10
+ // creators can't fumble a 40-char paste (why web-paste was rejected).
11
+ import { spawn } from 'node:child_process';
12
+ import { clearToken, setToken, getToken, getApiBase, DEFAULT_API_BASE } from './config.mjs';
13
+
14
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
15
+
16
+ function defaultOpenBrowser(url) {
17
+ const [cmd, args] =
18
+ process.platform === 'darwin'
19
+ ? ['open', [url]]
20
+ : process.platform === 'win32'
21
+ ? ['cmd', ['/c', 'start', '', url]]
22
+ : ['xdg-open', [url]];
23
+ try {
24
+ spawn(cmd, args, { stdio: 'ignore', detached: true }).unref();
25
+ } catch {
26
+ // No browser available (headless) — the printed URL is the fallback.
27
+ }
28
+ }
29
+
30
+ async function postJson(url, body) {
31
+ const res = await fetch(url, {
32
+ method: 'POST',
33
+ headers: { 'Content-Type': 'application/json' },
34
+ body: JSON.stringify(body ?? {}),
35
+ });
36
+ return { status: res.status, body: await res.json().catch(() => ({})) };
37
+ }
38
+
39
+ export async function login({
40
+ apiBase = getApiBase(DEFAULT_API_BASE),
41
+ log = console.log,
42
+ openBrowser = defaultOpenBrowser,
43
+ pollIntervalMsOverride,
44
+ } = {}) {
45
+ const { status, body: grant } = await postJson(`${apiBase}/api/device/code`);
46
+ if (status !== 200 || !grant.device_code) {
47
+ throw new Error(`could not start login against ${apiBase} (HTTP ${status})`);
48
+ }
49
+
50
+ log('');
51
+ log(' To connect this computer to your Joystack account:');
52
+ log('');
53
+ log(` 1. Open ${grant.verification_uri}`);
54
+ log(` 2. Enter ${grant.user_code}`);
55
+ log('');
56
+ log(' (opening your browser…)');
57
+ openBrowser(grant.verification_uri_complete ?? grant.verification_uri);
58
+
59
+ const intervalMs = pollIntervalMsOverride ?? (grant.interval ?? 5) * 1000;
60
+ const deadline = Date.now() + (grant.expires_in ?? 900) * 1000;
61
+ while (Date.now() < deadline) {
62
+ await sleep(intervalMs);
63
+ const poll = await postJson(`${apiBase}/api/device/token`, { device_code: grant.device_code });
64
+ if (poll.status === 200 && poll.body.token) {
65
+ setToken(poll.body.token, { apiBase, userId: poll.body.user_id });
66
+ log('');
67
+ log(` ✅ Logged in. This machine now builds and publishes as your account (${poll.body.user_id}).`);
68
+ return { userId: poll.body.user_id };
69
+ }
70
+ if (poll.body.error === 'authorization_pending') continue;
71
+ if (poll.body.error === 'expired_token') {
72
+ throw new Error('that login code expired before it was approved — run `joystack login` again for a fresh one.');
73
+ }
74
+ throw new Error(`login failed: ${poll.body.error ?? `HTTP ${poll.status}`}`);
75
+ }
76
+ throw new Error('login timed out — run `joystack login` again.');
77
+ }
78
+
79
+ export async function whoami({ apiBase = getApiBase(DEFAULT_API_BASE), log = console.log } = {}) {
80
+ const token = getToken();
81
+ if (!token) throw new Error('not logged in — run `joystack login` first.');
82
+ const res = await fetch(`${apiBase}/api/me`, { headers: { Authorization: `Bearer ${token}` } });
83
+ if (res.status === 401) throw new Error('stored token was rejected (unauthenticated) — run `joystack login` again.');
84
+ if (!res.ok) throw new Error(`whoami failed: HTTP ${res.status}`);
85
+ const me = await res.json();
86
+ log(`${me.name ?? me.userId} (${me.userId}) via ${apiBase}`);
87
+ return me;
88
+ }
89
+
90
+ // `joystack logout [--all]`: revokes the stored token on the platform, or with
91
+ // `all` every token of the account (every machine it is signed in on), then
92
+ // forgets it here. The local copy is forgotten even when the platform cannot be
93
+ // reached, and the output says what may still be valid in that case.
94
+ export async function logout({ all = false, apiBase = getApiBase(DEFAULT_API_BASE), log = console.log, timeoutMs = 15_000 } = {}) {
95
+ const token = getToken();
96
+ if (!token) {
97
+ log('Not logged in on this machine.');
98
+ return { revoked: 0 };
99
+ }
100
+ let revoked = null;
101
+ let rejected = false;
102
+ let problem = null;
103
+ try {
104
+ const res = await fetch(`${apiBase}/api/creator/logout`, {
105
+ method: 'POST',
106
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
107
+ body: JSON.stringify({ all }),
108
+ signal: AbortSignal.timeout(timeoutMs),
109
+ });
110
+ if (res.ok) {
111
+ const body = await res.json().catch(() => null);
112
+ revoked = typeof body?.revoked === 'number' ? body.revoked : null;
113
+ } else if (res.status === 401) {
114
+ rejected = true;
115
+ } else {
116
+ problem = `HTTP ${res.status}`;
117
+ }
118
+ } catch (err) {
119
+ problem = err?.message ?? String(err);
120
+ }
121
+ clearToken();
122
+ if (problem) {
123
+ log(`Logged out on this machine, but ${apiBase} could not revoke the token (${problem}); it stays valid until it expires.`);
124
+ } else if (rejected && all) {
125
+ log('Logged out on this machine. Its token had already expired or been revoked, so other machines were not signed out: run `joystack login`, then `joystack logout --all`.');
126
+ } else if (rejected) {
127
+ log('Logged out: the token had already expired or been revoked, and is removed from this machine.');
128
+ } else if (all) {
129
+ log(revoked === null ? 'Logged out everywhere.' : `Logged out everywhere: ${revoked} token${revoked === 1 ? '' : 's'} revoked.`);
130
+ } else {
131
+ log('Logged out: the token is revoked and removed from this machine.');
132
+ }
133
+ return { revoked, rejected, problem };
134
+ }
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ // Thin executable wrapper over the model baker. The normal front door is
3
+ // `joystack model bake <glb>` (bin/joystack.mjs); this file exists so other tooling
4
+ // can exec the same implementation directly. One baker, two entrances.
5
+ import { bakeModel } from './model-bake.mjs';
6
+
7
+ const glb = process.argv[2];
8
+ if (!glb) {
9
+ console.error('usage: model-bake <file.glb> (bakes <file>.skeleton.json + <file>.mesh.json beside it)');
10
+ process.exit(2);
11
+ }
12
+ try {
13
+ await bakeModel(glb);
14
+ } catch (err) {
15
+ console.error(`model bake: ${err.message}`);
16
+ process.exit(1);
17
+ }