@playcraft/cli 0.0.48 → 0.0.51
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.
- package/README.md +65 -44
- package/REMIX.md +757 -0
- package/dist/commands/login.js +82 -32
- package/dist/commands/remix.js +883 -176
- package/dist/utils/playcraft-http-client.js +132 -0
- package/dist/utils/remix-image-attachments.js +125 -0
- package/package.json +7 -3
- package/dist/agent/cloud-connection.test.js +0 -67
- package/dist/agent/local-backend.test.js +0 -52
- package/dist/commands/fix-ids.test.js +0 -264
- package/dist/commands/tools-research.test.js +0 -216
- package/dist/prad/check.test.js +0 -49
- package/dist/sync/sync-manager.test.js +0 -80
package/dist/commands/login.js
CHANGED
|
@@ -46,16 +46,37 @@ function resolveLoginBaseUrl(options) {
|
|
|
46
46
|
DEFAULT_PLAYCRAFT_CLOUD_BASE;
|
|
47
47
|
return raw.replace(/\/+$/, '');
|
|
48
48
|
}
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
function normalizeLoginEnvironmentUrl(raw) {
|
|
50
|
+
try {
|
|
51
|
+
const url = new URL(raw);
|
|
52
|
+
url.hash = '';
|
|
53
|
+
url.search = '';
|
|
54
|
+
url.pathname = url.pathname.replace(/\/+$/, '');
|
|
55
|
+
return url.toString().replace(/\/+$/, '');
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export function resolveExistingTokenForLoginTarget(globalConfig, backendUrl) {
|
|
62
|
+
const token = typeof globalConfig.token === 'string' ? globalConfig.token : undefined;
|
|
63
|
+
const storedBackendUrl = typeof globalConfig.backendUrl === 'string'
|
|
64
|
+
? globalConfig.backendUrl
|
|
65
|
+
: typeof globalConfig.url === 'string'
|
|
66
|
+
? globalConfig.url
|
|
67
|
+
: undefined;
|
|
68
|
+
if (!token || !storedBackendUrl)
|
|
69
|
+
return undefined;
|
|
70
|
+
const storedEnvironment = normalizeLoginEnvironmentUrl(storedBackendUrl);
|
|
71
|
+
const targetEnvironment = normalizeLoginEnvironmentUrl(backendUrl);
|
|
72
|
+
return storedEnvironment && targetEnvironment && storedEnvironment === targetEnvironment
|
|
73
|
+
? token
|
|
74
|
+
: undefined;
|
|
51
75
|
}
|
|
52
76
|
export function buildCliAuthUrl(options) {
|
|
53
77
|
const url = new URL('/cli-auth', options.backendUrl);
|
|
54
78
|
url.searchParams.set('callback', options.callbackUrl);
|
|
55
79
|
url.searchParams.set('state', options.state);
|
|
56
|
-
if (options.existingToken?.startsWith(PAT_PREFIX)) {
|
|
57
|
-
url.searchParams.set('existingTokenHash', createTokenHash(options.existingToken));
|
|
58
|
-
}
|
|
59
80
|
return url.toString();
|
|
60
81
|
}
|
|
61
82
|
export async function renewExistingToken(options) {
|
|
@@ -80,8 +101,9 @@ export async function renewExistingToken(options) {
|
|
|
80
101
|
}
|
|
81
102
|
export async function loginCommand(options) {
|
|
82
103
|
const backendUrl = resolveLoginBaseUrl(options);
|
|
104
|
+
const callbackPort = options.callbackPort ?? CALLBACK_PORT;
|
|
83
105
|
const globalConfig = loadGlobalConfig();
|
|
84
|
-
const existingToken =
|
|
106
|
+
const existingToken = resolveExistingTokenForLoginTarget(globalConfig, backendUrl);
|
|
85
107
|
if (existingToken) {
|
|
86
108
|
const renewed = await renewExistingToken({ backendUrl, token: existingToken });
|
|
87
109
|
if (renewed) {
|
|
@@ -96,6 +118,7 @@ export async function loginCommand(options) {
|
|
|
96
118
|
}
|
|
97
119
|
const state = crypto.randomBytes(16).toString('hex');
|
|
98
120
|
return new Promise((resolve, reject) => {
|
|
121
|
+
let timeout;
|
|
99
122
|
const server = http.createServer((req, res) => {
|
|
100
123
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
101
124
|
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
|
|
@@ -112,34 +135,41 @@ export async function loginCommand(options) {
|
|
|
112
135
|
try {
|
|
113
136
|
const data = JSON.parse(body);
|
|
114
137
|
if (data.state !== state) {
|
|
115
|
-
res.writeHead(403, {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
138
|
+
res.writeHead(403, {
|
|
139
|
+
'Content-Type': 'text/html',
|
|
140
|
+
Connection: 'close',
|
|
141
|
+
});
|
|
142
|
+
res.end('<html><body><h1>Invalid state</h1></body></html>', () => {
|
|
143
|
+
closeServer(() => {
|
|
144
|
+
reject(new Error('State mismatch - possible CSRF attack'));
|
|
145
|
+
});
|
|
119
146
|
});
|
|
120
147
|
return;
|
|
121
148
|
}
|
|
122
149
|
const callbackToken = typeof data.token === 'string' ? data.token : undefined;
|
|
123
|
-
|
|
124
|
-
const tokenToSave = callbackToken || renewedToken;
|
|
125
|
-
if (!tokenToSave) {
|
|
150
|
+
if (!callbackToken) {
|
|
126
151
|
throw new Error('Login callback did not include a token');
|
|
127
152
|
}
|
|
128
153
|
saveGlobalConfig({
|
|
129
|
-
token:
|
|
154
|
+
token: callbackToken,
|
|
130
155
|
url: backendUrl,
|
|
131
156
|
backendUrl,
|
|
132
157
|
});
|
|
133
|
-
res.writeHead(200, {
|
|
158
|
+
res.writeHead(200, {
|
|
159
|
+
'Content-Type': 'text/html',
|
|
160
|
+
Connection: 'close',
|
|
161
|
+
});
|
|
134
162
|
res.end('<html>' +
|
|
135
163
|
'<head><meta charset="utf-8"><title>Login Success</title></head>' +
|
|
136
164
|
'<body style="font-family:system-ui;text-align:center;padding:40px">' +
|
|
137
165
|
'<h1>CLI Login Successful</h1>' +
|
|
138
166
|
'<p>You can close this window and return to your terminal.</p>' +
|
|
139
|
-
'</body></html>')
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
167
|
+
'</body></html>', () => {
|
|
168
|
+
closeServer(() => {
|
|
169
|
+
console.log('Login successful! Token saved to ~/.playcraft/config.json');
|
|
170
|
+
resolve();
|
|
171
|
+
});
|
|
172
|
+
});
|
|
143
173
|
}
|
|
144
174
|
catch (err) {
|
|
145
175
|
const cause = err instanceof Error ? err : new Error(String(err));
|
|
@@ -150,23 +180,39 @@ export async function loginCommand(options) {
|
|
|
150
180
|
const userFacing = cause instanceof SyntaxError
|
|
151
181
|
? 'Invalid JSON in login callback.'
|
|
152
182
|
: `Login callback failed: ${cause.message}`;
|
|
153
|
-
res.writeHead(500, {
|
|
183
|
+
res.writeHead(500, {
|
|
184
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
185
|
+
Connection: 'close',
|
|
186
|
+
});
|
|
154
187
|
res.end('<html><head><meta charset="utf-8"/><title>Error</title></head>' +
|
|
155
188
|
'<body style="font-family:system-ui;padding:24px">' +
|
|
156
189
|
`<h1>Error</h1><p>${escapeHtml(userFacing)}</p>` +
|
|
157
|
-
'</body></html>')
|
|
158
|
-
|
|
159
|
-
|
|
190
|
+
'</body></html>', () => {
|
|
191
|
+
closeServer(() => {
|
|
192
|
+
reject(new Error(userFacing));
|
|
193
|
+
});
|
|
160
194
|
});
|
|
161
195
|
}
|
|
162
196
|
});
|
|
163
197
|
}
|
|
164
198
|
});
|
|
199
|
+
const closeServer = (onClosed) => {
|
|
200
|
+
if (timeout) {
|
|
201
|
+
clearTimeout(timeout);
|
|
202
|
+
timeout = undefined;
|
|
203
|
+
}
|
|
204
|
+
if (!server.listening) {
|
|
205
|
+
onClosed();
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
server.close(onClosed);
|
|
209
|
+
server.closeAllConnections();
|
|
210
|
+
};
|
|
165
211
|
// Bind failures emit 'error' (e.g. EADDRINUSE); synchronously thrown errors from listen() are caught below.
|
|
166
212
|
server.on('error', (err) => {
|
|
167
|
-
|
|
213
|
+
closeServer(() => {
|
|
168
214
|
if (err.code === 'EADDRINUSE') {
|
|
169
|
-
reject(new Error(`Port ${
|
|
215
|
+
reject(new Error(`Port ${callbackPort} is already in use. ` +
|
|
170
216
|
'Please close any other playcraft login process and try again.'));
|
|
171
217
|
}
|
|
172
218
|
else {
|
|
@@ -175,13 +221,14 @@ export async function loginCommand(options) {
|
|
|
175
221
|
});
|
|
176
222
|
});
|
|
177
223
|
try {
|
|
178
|
-
server.listen(
|
|
179
|
-
const
|
|
224
|
+
server.listen(callbackPort, () => {
|
|
225
|
+
const address = server.address();
|
|
226
|
+
const listeningPort = typeof address === 'object' && address ? address.port : callbackPort;
|
|
227
|
+
const callbackUrl = `http://localhost:${listeningPort}/callback`;
|
|
180
228
|
const authUrl = buildCliAuthUrl({
|
|
181
229
|
backendUrl,
|
|
182
230
|
callbackUrl,
|
|
183
231
|
state,
|
|
184
|
-
existingToken,
|
|
185
232
|
});
|
|
186
233
|
console.log('Opening browser for login...');
|
|
187
234
|
console.log(`If the browser does not open, visit this URL manually:\n ${authUrl}\n`);
|
|
@@ -190,19 +237,22 @@ export async function loginCommand(options) {
|
|
|
190
237
|
});
|
|
191
238
|
}
|
|
192
239
|
catch (err) {
|
|
193
|
-
|
|
240
|
+
closeServer(() => {
|
|
194
241
|
reject(err instanceof Error ? err : new Error(String(err)));
|
|
195
242
|
});
|
|
196
243
|
return;
|
|
197
244
|
}
|
|
198
245
|
// Timeout handler
|
|
199
|
-
|
|
200
|
-
|
|
246
|
+
timeout = setTimeout(() => {
|
|
247
|
+
closeServer(() => {
|
|
201
248
|
reject(new Error('Login timed out after 5 minutes. Please try again.'));
|
|
202
249
|
});
|
|
203
250
|
}, TIMEOUT_MS);
|
|
204
251
|
server.on('close', () => {
|
|
205
|
-
|
|
252
|
+
if (timeout) {
|
|
253
|
+
clearTimeout(timeout);
|
|
254
|
+
timeout = undefined;
|
|
255
|
+
}
|
|
206
256
|
});
|
|
207
257
|
});
|
|
208
258
|
}
|