Setting the file. One moment.
Bootstrap · Wix Headless Kit · wix/skills · Skills Docs
ContentsBack to the top of the page — line 153
This file
Number 9.10
Position 10 of 20
Type JavaScript
Size 8 KB
Lines 175 install/ bootstrap.mjs
JavaScript · 175 lines · 8 KB
,
import
.
meta
.url),
"utf8"
));
7 const WIX_CLI = `@wix/cli@${ PINS [ "@wix/cli" ] }` ;
8
9 // ── tiny event protocol (one JSON object per line) ───────────────────────────
10 const emit = ( event , extra = {}) =>
11 process.stdout. write ( JSON . stringify ({ event, ... extra }) + ' \n ' );
12 const fail = ( event , extra = {}) => {
13 emit (event, { ok: false , ... extra });
14 process. exit ( 1 );
15 };
16
17 // ── npx on every platform ────────────────────────────────────────────────────
18 // On Windows npx is a .cmd shim, which Node starts only through a shell; a detached shell opens its
19 // own console, and the login's JSON events never reach the log. npm ships npx's JS entry beside
20 // node.exe, and that starts directly with node: no shell, no window, and the pid is the CLI's.
21 // A Node without npm beside it (a version manager's shim) keeps the .cmd through the shell.
22 const isWin = process.platform === 'win32' ;
23 const NPX_JS = isWin ? join ( dirname (process.execPath), 'node_modules' , 'npm' , 'bin' , 'npx-cli.js' ) : null ;
24 const NPX = NPX_JS && existsSync ( NPX_JS )
25 ? { file: process.execPath, pre: [ NPX_JS ], shell: false }
26 : { file: isWin ? 'npx.cmd' : 'npx' , pre: [], shell: isWin };
27 const WIX = [ NPX .file, ... NPX .pre, '-y' , WIX_CLI ]; // run the CLI via npx — no global install/mutation
28
29 // Force the CLI into non-interactive "agent" mode. Without an agent signal in
30 // the env, `wix login` renders an interactive Ink TUI (device code + keypress)
31 // that needs a raw TTY and crashes in an agent sandbox — and it never emits the
32 // JSON login events this script forwards. The CLI uses @vercel/detect-agent,
33 // whose first check is the AI_AGENT env var, so setting it guarantees agent mode
34 // (and the awaiting_user/success/logged_in events) for every child we spawn.
35 // Respect an existing value so a known runner (claude, cursor, …) keeps its name.
36 const AGENT_ENV = { ... process.env, AI_AGENT: process.env. AI_AGENT || 'wix-headless-kit-skill' };
37
38 // Where the detached login parks its output between runs. It has to outlive this
39 // process, so it can't be a pipe and can't live in the project.
40 const STATE_DIR = join ( tmpdir (), 'wix-headless-kit-login' );
41 const LOG = join ( STATE_DIR , 'login.log' );
42 const PIDFILE = join ( STATE_DIR , 'login.pid' );
43
44 // run a command, capture stdout+stderr (combined), return {status, out}
45 function capture ( cmd , args , opts = {}) {
46 const r = spawnSync (cmd, args, { encoding: 'utf8' , shell: NPX .shell, windowsHide: true , env: AGENT_ENV , ... opts });
47 return { status: r.status ?? 1 , out: `${ r . stdout || ''}${ r . stderr || ''}` , error: r.error };
48 }
49
50 // ── 1. CLI reachable (via npx — no install) ──────────────────────────────────
51 // A line holding only a version is the CLI's answer ("npm notice" lines carry versions too).
52 const VERSION_LINE = / ^ \s * ( \d + \. \d + \. \d + \S * ) \s *$ / m ;
53 const answered = ( r ) => r.status === 0 || VERSION_LINE . test (r.out);
54
55 function checkCli () {
56 let r = capture ( WIX [ 0 ], [ ... WIX . slice ( 1 ), '--version' ]);
57 // On a cold npm cache npx can exit non-zero after the version, the install's warnings around
58 // it; a second run finds the cache warm.
59 if ( ! answered (r)) r = capture ( WIX [ 0 ], [ ... WIX . slice ( 1 ), '--version' ]);
60 if ( ! answered (r)) {
61 fail ( 'cli_unreachable' , { detail: (r.out. trim () || String (r.error?.message || `exit ${ r . status }` )). slice ( 0 , 400 ) });
62 }
63 // npx interleaves "npm notice …" lines with the version, so don't just take the
64 // last line — pick the version line, else the first semver-looking token.
65 const version = r.out. match ( VERSION_LINE )?.[ 1 ] || (r.out. match ( / \d + \. \d + \. \d + [ ^ \s] * / ) || [])[ 0 ] || r.out. trim (). split ( ' \n ' ). pop ();
66 emit ( 'cli_ok' , { version });
67 }
68
69 // ── 2. Reuse an existing session, or start device login ──────────────────────
70 const hasExistingSession = () => capture ( WIX [ 0 ], [ ... WIX . slice ( 1 ), 'whoami' ]).status === 0 ;
71
72 function loginEvents () {
73 if ( ! existsSync ( LOG )) return [];
74 return readFileSync ( LOG , 'utf8' )
75 . split ( ' \n ' )
76 . map (( l ) => l. trim ())
77 . filter (Boolean)
78 . map (( l ) => {
79 try {
80 return JSON . parse (l);
81 } catch {
82 return null ; // non-JSON CLI chatter
83 }
84 })
85 . filter (( e ) => e && e.event);
86 }
87
88 const alive = ( pid ) => {
89 try {
90 process. kill (pid, 0 );
91 return true ;
92 } catch {
93 return false ;
94 }
95 };
96
97 // A login this machine started earlier may still be waiting on the browser. Reuse
98 // its code instead of minting a second one — a new login invalidates nothing, but
99 // it hands the user a different code than the one already on their screen.
100 function pendingLogin () {
101 if ( ! existsSync ( PIDFILE )) return null ;
102 let state;
103 try {
104 state = JSON . parse ( readFileSync ( PIDFILE , 'utf8' ));
105 } catch {
106 return null ;
107 }
108 if ( ! state.pid || ! alive (state.pid)) return null ;
109 const ev = loginEvents (). find (( e ) => e.event === 'awaiting_user' );
110 if ( ! ev) return null ;
111 // Don't hand back a code that's about to expire mid-typing.
112 const ageSeconds = Math. round ((Date. now () - state.startedAt) / 1000 );
113 if (ageSeconds > (ev.expiresInSeconds ?? 600 ) - 60 ) return null ;
114 return ev;
115 }
116
117 // Detach so the login keeps polling after this process exits. stdio goes to a
118 // file, not a pipe: a pipe dies with the parent, and the CLI would get EPIPE.
119 function startLogin () {
120 mkdirSync ( STATE_DIR , { recursive: true });
121 writeFileSync ( LOG , '' );
122 const out = openSync ( LOG , 'a' );
123 const child = spawn ( WIX [ 0 ], [ ... WIX . slice ( 1 ), 'login' ], {
124 detached: true ,
125 stdio: [ 'ignore' , out, out],
126 env: AGENT_ENV ,
127 shell: NPX .shell,
128 windowsHide: true ,
129 });
130 child. on ( 'error' , ( e ) => fail ( 'login_failed' , { detail: String (e) }));
131 writeFileSync ( PIDFILE , JSON . stringify ({ pid: child.pid, startedAt: Date. now () }));
132 child. unref ();
133 }
134
135 async function waitForCode ( timeoutMs = 60000 ) {
136 const deadline = Date. now () + timeoutMs;
137 while (Date. now () < deadline) {
138 const events = loginEvents ();
139 const ev = events. find (( e ) => e.event === 'awaiting_user' );
140 if (ev) return ev;
141 const bad = events. find (( e ) => e.event === 'login_failed' );
142 if (bad) fail ( 'login_failed' , bad);
143 await new Promise (( r ) => setTimeout (r, 500 ));
144 }
145 const detail = existsSync ( LOG ) ? readFileSync ( LOG , 'utf8' ). trim (). slice ( - 1500 ) : '' ;
146 fail ( 'login_failed' , {
147 detail: detail || `wix login wrote nothing within ${ timeoutMs / 1000 }s; \` ${ [ 'npx' , '-y' , WIX . at ( - 1 ), 'login' ]. join ( ' ' ) } \` run in the foreground prints the code.` ,
148 });
149 }
150
151 // The next move is the user's, so hand the code back and exit rather than holding
152 // the caller open for a browser round-trip. `message` is the sentence to relay.
153 function surrenderTo ( ev ) {
154 emit ( 'awaiting_user' , {
155 ... ev,
156 message:
157 `To connect your Wix account, open ${ ev . verificationUri } and enter the code ` +
158 `${ ev . userCode }. Tell me once you're done and I'll continue.` ,
159 });
160 process. exit ( 0 );
161 }
162
163 // ── main ────────────────────────────────────────────────────────────────────
164 checkCli ();
165
166 if ( hasExistingSession ()) {
167 emit ( 'logged_in' );
168 process. exit ( 0 );
169 }
170
171 const pending = pendingLogin ();
172 if (pending) surrenderTo (pending);
173
174 startLogin ();
175 surrenderTo ( await waitForCode ());