orizu 0.5.49 → 0.5.50

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 (74) hide show
  1. package/dist/account-directory.d.ts +2 -2
  2. package/dist/account-directory.d.ts.map +1 -1
  3. package/dist/account-directory.js +7 -32
  4. package/dist/auth-commands.d.ts.map +1 -1
  5. package/dist/auth-commands.js +22 -30
  6. package/dist/credentials.d.ts +5 -0
  7. package/dist/credentials.d.ts.map +1 -1
  8. package/dist/credentials.js +84 -3
  9. package/dist/dataset-transfer-upload.d.ts.map +1 -1
  10. package/dist/dataset-transfer-upload.js +6 -1
  11. package/dist/directory-list-read.d.ts +37 -0
  12. package/dist/directory-list-read.d.ts.map +1 -0
  13. package/dist/directory-list-read.js +87 -0
  14. package/dist/hosted-boot.d.ts.map +1 -1
  15. package/dist/hosted-boot.js +59 -41
  16. package/dist/hosted-harness-opencode.d.ts +31 -0
  17. package/dist/hosted-harness-opencode.d.ts.map +1 -1
  18. package/dist/hosted-harness-opencode.js +45 -7
  19. package/dist/hosted-loop.d.ts +39 -2
  20. package/dist/hosted-loop.d.ts.map +1 -1
  21. package/dist/hosted-loop.js +127 -18
  22. package/dist/hosted-session-seed.d.ts +2 -0
  23. package/dist/hosted-session-seed.d.ts.map +1 -1
  24. package/dist/hosted-session-seed.js +27 -10
  25. package/dist/http.d.ts.map +1 -1
  26. package/dist/http.js +39 -234
  27. package/dist/keyboard-select.d.ts.map +1 -1
  28. package/dist/keyboard-select.js +23 -45
  29. package/dist/project-create-workflow.d.ts +18 -0
  30. package/dist/project-create-workflow.d.ts.map +1 -0
  31. package/dist/project-create-workflow.js +121 -0
  32. package/dist/self-update-command.d.ts +8 -0
  33. package/dist/self-update-command.d.ts.map +1 -0
  34. package/dist/self-update-command.js +390 -0
  35. package/dist/self-update.d.ts +29 -16
  36. package/dist/self-update.d.ts.map +1 -1
  37. package/dist/self-update.js +55 -244
  38. package/dist/setup-account.d.ts.map +1 -1
  39. package/dist/setup-account.js +4 -1
  40. package/dist/setup-and-skills-commands.d.ts.map +1 -1
  41. package/dist/setup-and-skills-commands.js +88 -102
  42. package/dist/skill-install-workflow.d.ts +23 -0
  43. package/dist/skill-install-workflow.d.ts.map +1 -0
  44. package/dist/skill-install-workflow.js +184 -0
  45. package/dist/skill-installer.d.ts +8 -3
  46. package/dist/skill-installer.d.ts.map +1 -1
  47. package/dist/skill-installer.js +26 -33
  48. package/dist/stored-session-requests.d.ts +27 -0
  49. package/dist/stored-session-requests.d.ts.map +1 -0
  50. package/dist/stored-session-requests.js +264 -0
  51. package/dist/terminal-key-session.d.ts +23 -0
  52. package/dist/terminal-key-session.d.ts.map +1 -0
  53. package/dist/terminal-key-session.js +74 -0
  54. package/package.json +1 -1
  55. package/src/account-directory.ts +8 -42
  56. package/src/auth-commands.ts +20 -29
  57. package/src/credentials.ts +83 -3
  58. package/src/dataset-transfer-upload.ts +7 -1
  59. package/src/directory-list-read.ts +123 -0
  60. package/src/hosted-boot.ts +65 -43
  61. package/src/hosted-harness-opencode.ts +65 -9
  62. package/src/hosted-loop.ts +152 -18
  63. package/src/hosted-session-seed.ts +25 -8
  64. package/src/http.ts +39 -317
  65. package/src/keyboard-select.ts +18 -55
  66. package/src/project-create-workflow.ts +139 -0
  67. package/src/self-update-command.ts +419 -0
  68. package/src/self-update.ts +70 -266
  69. package/src/setup-account.ts +4 -1
  70. package/src/setup-and-skills-commands.ts +89 -121
  71. package/src/skill-install-workflow.ts +215 -0
  72. package/src/skill-installer.ts +31 -38
  73. package/src/stored-session-requests.ts +316 -0
  74. package/src/terminal-key-session.ts +88 -0
@@ -16,6 +16,8 @@
16
16
  * Credentials come only through the `-c` settings the caller passes (the
17
17
  * boot's credential helper), never in a URL or argument.
18
18
  */
19
+ import { existsSync, readdirSync, renameSync } from 'fs';
20
+ import { join } from 'path';
19
21
  export const SESSION_COPY_SEED_FAILURE = 'session copy could not be filled from the team copy';
20
22
  function withConfig(config) {
21
23
  return config.flatMap(setting => ['-c', setting]);
@@ -120,16 +122,31 @@ export async function cloneTeamIntoWorkTree(input) {
120
122
  const pointed = input.exec('git', ['-C', input.scratchDir, 'remote', 'set-url', 'origin', input.sessionRemote]);
121
123
  if (pointed.status !== 0)
122
124
  throw failure('pointing the working folder at the session copy', pointed);
123
- // The runtime image may ship the folder empty; `mv` needs it gone.
124
- input.exec('rmdir', ['--', input.workTree]);
125
- // A folder that is still there (not empty, or a mount point) would make
126
- // `mv` put the clone inside it.
127
- const remaining = input.exec('test', ['-e', input.workTree]);
128
- if (remaining.status === 0)
129
- throw failure('making the working folder', { status: 1, stdout: '', stderr: `${input.workTree} is not empty` });
130
- const moved = input.exec('mv', ['--', input.scratchDir, input.workTree]);
131
- if (moved.status !== 0)
132
- throw failure('making the working folder', moved);
125
+ // ORI-2512: an existing folder (the runtime image may ship it empty, and
126
+ // the boot may have started OpenCode in it) is filled in place, never
127
+ // replaced: a process already started there would be left in a deleted
128
+ // folder. A missing folder becomes the clone.
129
+ if (existsSync(input.workTree)) {
130
+ if (readdirSync(input.workTree).length > 0)
131
+ throw failure('making the working folder', { status: 1, stdout: '', stderr: `${input.workTree} is not empty` });
132
+ try {
133
+ // `.git` last: a fill that stops partway leaves no repository, which
134
+ // the next boot refuses, rather than one missing files that a save
135
+ // would push as deleted.
136
+ const entries = readdirSync(input.scratchDir).sort((a, b) => Number(a === '.git') - Number(b === '.git'));
137
+ const rename = input.rename ?? renameSync;
138
+ for (const entry of entries)
139
+ rename(join(input.scratchDir, entry), join(input.workTree, entry));
140
+ }
141
+ catch (error) {
142
+ throw failure('making the working folder', { status: 1, stdout: '', stderr: error instanceof Error ? error.message : String(error) });
143
+ }
144
+ }
145
+ else {
146
+ const moved = input.exec('mv', ['--', input.scratchDir, input.workTree]);
147
+ if (moved.status !== 0)
148
+ throw failure('making the working folder', moved);
149
+ }
133
150
  }
134
151
  finally {
135
152
  clear();
@@ -1 +1 @@
1
- {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAWA,OAAO,EAAsC,WAAW,EAAoB,MAAM,mBAAmB,CAAA;AAerG,wBAAgB,sBAAsB,IAAI,MAAM,GAAG,IAAI,CAEtD;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,WAAW,QAGhD;AAED,wBAAgB,cAAc,CAAC,KAAK,GAAE,WAA0B,GAAG,MAAM,CAiBxE;AAED,UAAU,YAAY;IACpB,OAAO,EAAE,MAAM,CAAA;IACf,iBAAiB,EAAE,MAAM,CAAA;CAC1B;AAID,wBAAgB,mBAAmB,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,YAAY,CAMnE;AAED,UAAU,WAAW;IACnB,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,OAAO,CAAA;CACnB;AAED,wBAAgB,kBAAkB,CAAC,KAAK,GAAE,WAA0B,GAAG,WAAW,CAajF;AAED,wBAAgB,mBAAmB,CAAC,KAAK,GAAE,WAA0B,GAAG,MAAM,CAE7E;AAED,wBAAgB,UAAU,IAAI,MAAM,CAEnC;AAWD,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,QAmBzD;AA+FD,wBAAgB,+BAA+B,IAAI,IAAI,CAAC,WAAW,EAAE,UAAU,CAAC,CAE/E;AAmGD,MAAM,MAAM,0BAA0B,GAAG,aAAa,GAAG,kBAAkB,GAAG,QAAQ,CAAA;AAEtF,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,MAAM,EAAE,0BAA0B,CAAA;IAC3C,sFAAsF;IACtF,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,EAAE,cAAc,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;IACvF,MAAM,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,IAAI,GAAG,OAAO,CAAC,YAAY,CAAC,CAAA;CAC3D;AAED,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,sBAAsB,EAAE,OAAO,CAAA;CAChC;AAED;;;;;GAKG;AACH,wBAAgB,kCAAkC,CAChD,OAAO,GAAE;IAAE,YAAY,CAAC,EAAE,OAAO,CAAC;IAAC,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAAO,GACpE,2BAA2B,CAoI7B;AAID,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,WAAgB,EAAE,cAAc,CAAC,EAAE,MAAM,IAAI,qBAqElG"}
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAOA,OAAO,EAAsC,WAAW,EAAoB,MAAM,mBAAmB,CAAA;AAWrG,wBAAgB,sBAAsB,IAAI,MAAM,GAAG,IAAI,CAEtD;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,WAAW,QAGhD;AAED,wBAAgB,cAAc,CAAC,KAAK,GAAE,WAA0B,GAAG,MAAM,CAiBxE;AAED,UAAU,YAAY;IACpB,OAAO,EAAE,MAAM,CAAA;IACf,iBAAiB,EAAE,MAAM,CAAA;CAC1B;AAID,wBAAgB,mBAAmB,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,YAAY,CAMnE;AAED,UAAU,WAAW;IACnB,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,OAAO,CAAA;CACnB;AAED,wBAAgB,kBAAkB,CAAC,KAAK,GAAE,WAA0B,GAAG,WAAW,CAajF;AAED,wBAAgB,mBAAmB,CAAC,KAAK,GAAE,WAA0B,GAAG,MAAM,CAE7E;AAED,wBAAgB,UAAU,IAAI,MAAM,CAEnC;AAWD,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,QAmBzD;AAED,wBAAgB,+BAA+B,IAAI,IAAI,CAAC,WAAW,EAAE,UAAU,CAAC,CAE/E;AAID,MAAM,MAAM,0BAA0B,GAAG,aAAa,GAAG,kBAAkB,GAAG,QAAQ,CAAA;AAEtF,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,MAAM,EAAE,0BAA0B,CAAA;IAC3C,sFAAsF;IACtF,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,EAAE,cAAc,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;IACvF,MAAM,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,IAAI,GAAG,OAAO,CAAC,YAAY,CAAC,CAAA;CAC3D;AAED,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,sBAAsB,EAAE,OAAO,CAAA;CAChC;AAMD;;;;;GAKG;AACH,wBAAgB,kCAAkC,CAChD,OAAO,GAAE;IAAE,YAAY,CAAC,EAAE,OAAO,CAAC;IAAC,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAAO,GACpE,2BAA2B,CAqE7B;AAcD,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,WAAgB,EAAE,cAAc,CAAC,EAAE,MAAM,IAAI,qBAoClG"}
package/dist/http.js CHANGED
@@ -1,6 +1,5 @@
1
- import { clearServerCredentialsIfCurrent, credentialsEqual, resolveCredentialServer, getActiveBaseUrl, getServerCredentials, rememberResolvedBearer, resolveEnvBearerToken, updateServerCredentialsIfCurrent, withCredentialsTransactionLockAsync, } from './credentials.js';
1
+ import { resolveCredentialServer, getActiveBaseUrl, getServerCredentials, rememberResolvedBearer, resolveEnvBearerToken, } from './credentials.js';
2
2
  import { canonicalServerUrl, getFlagBaseUrl, normalizeBaseUrl } from './global-flags.js';
3
- import { describeLogoutHttpFailure, describeLogoutTransportFailure, } from './logout-diagnostic.js';
4
3
  let runtimeFlags = { local: false, server: null };
5
4
  let lastResolvedBaseUrl = null;
6
5
  function rememberResolvedBaseUrl(baseUrl) {
@@ -78,141 +77,12 @@ export function assertSecureTokenTransport(baseUrl) {
78
77
  }
79
78
  throw new Error(`Refusing to send CLI tokens to ${baseUrl}. Use HTTPS, or --local for loopback development.`);
80
79
  }
81
- function isSessionCredentials(credentials) {
82
- return 'accessToken' in credentials;
83
- }
84
- function getAuthorizationToken(credentials) {
85
- return isSessionCredentials(credentials)
86
- ? rememberSessionCredentials(credentials).accessToken
87
- : rememberResolvedBearer(credentials.apiKey);
88
- }
89
- function rememberSessionCredentials(credentials) {
90
- rememberResolvedBearer(credentials.accessToken);
91
- rememberResolvedBearer(credentials.refreshToken);
92
- return credentials;
93
- }
94
- function isExpired(expiresAt) {
95
- const nowUnix = Math.floor(Date.now() / 1000);
96
- return expiresAt <= nowUnix + 30;
97
- }
98
- const DEFAULT_CREDENTIALS_REFRESH_TIMEOUT_MS = 10_000;
99
- const MAX_CREDENTIALS_REFRESH_TIMEOUT_MS = 30_000;
100
- const DEFAULT_LOGOUT_TIMEOUT_MS = 10_000;
101
- const MAX_LOGOUT_TIMEOUT_MS = 30_000;
102
- function getCredentialsRefreshTimeoutMs() {
103
- const configured = Number(process.env.ORIZU_CREDENTIALS_REFRESH_TIMEOUT_MS);
104
- if (!Number.isSafeInteger(configured) || configured <= 0) {
105
- return DEFAULT_CREDENTIALS_REFRESH_TIMEOUT_MS;
106
- }
107
- return Math.min(configured, MAX_CREDENTIALS_REFRESH_TIMEOUT_MS);
108
- }
109
- function getLogoutTimeoutMs() {
110
- const configured = Number(process.env.ORIZU_LOGOUT_TIMEOUT_MS);
111
- if (!Number.isSafeInteger(configured) || configured <= 0) {
112
- return DEFAULT_LOGOUT_TIMEOUT_MS;
113
- }
114
- return Math.min(configured, MAX_LOGOUT_TIMEOUT_MS);
115
- }
116
- async function withAbortDeadline(operation, callerSignal, timeoutMs, timeoutError) {
117
- const controller = new AbortController();
118
- const handleCallerAbort = () => controller.abort(callerSignal?.reason);
119
- if (callerSignal?.aborted)
120
- handleCallerAbort();
121
- else
122
- callerSignal?.addEventListener('abort', handleCallerAbort, { once: true });
123
- const timer = setTimeout(() => controller.abort(timeoutError), timeoutMs);
124
- const cancellation = new Promise((_resolve, reject) => {
125
- const handleAbort = () => reject(controller.signal.reason ?? new Error('Request aborted.'));
126
- if (controller.signal.aborted)
127
- handleAbort();
128
- else
129
- controller.signal.addEventListener('abort', handleAbort, { once: true });
130
- });
131
- try {
132
- return await Promise.race([operation(controller.signal), cancellation]);
133
- }
134
- finally {
135
- clearTimeout(timer);
136
- callerSignal?.removeEventListener('abort', handleCallerAbort);
137
- }
138
- }
139
- async function withCredentialsRefreshDeadline(operation, callerSignal) {
140
- const timeoutMs = getCredentialsRefreshTimeoutMs();
141
- const timeoutError = new Error(`ORIZU_CREDENTIALS_REFRESH_TIMEOUT: session refresh exceeded ${timeoutMs}ms.`);
142
- return await withAbortDeadline(operation, callerSignal, timeoutMs, timeoutError);
143
- }
144
- async function withLogoutDeadline(operation, callerSignal) {
145
- const timeoutMs = getLogoutTimeoutMs();
146
- const timeoutError = new Error(`ORIZU_LOGOUT_TIMEOUT: remote logout exceeded ${timeoutMs}ms.`);
147
- timeoutError.name = 'TimeoutError';
148
- return await withAbortDeadline(operation, callerSignal, timeoutMs, timeoutError);
149
- }
150
80
  export function credentialRequestRedirectPolicy() {
151
81
  return process.env.ORIZU_CLI_AUTH_REDIRECT === 'error' ? { redirect: 'error' } : {};
152
82
  }
153
- async function refreshCredentials(baseUrl, credentials, callerSignal) {
154
- if (!isSessionCredentials(credentials)) {
155
- throw new Error('API key credentials do not refresh. Run `orizu login` again if access fails.');
156
- }
157
- assertSecureTokenTransport(baseUrl);
158
- const refreshToken = rememberResolvedBearer(credentials.refreshToken);
159
- const data = await withCredentialsRefreshDeadline(async (signal) => {
160
- const response = await fetch(`${baseUrl}/api/cli/auth/refresh`, {
161
- method: 'POST',
162
- headers: { 'Content-Type': 'application/json' },
163
- body: JSON.stringify({ refreshToken }),
164
- ...credentialRequestRedirectPolicy(),
165
- signal,
166
- });
167
- if (!response.ok) {
168
- throw new Error('Session expired. Run `orizu login` again.');
169
- }
170
- return await response.json();
171
- }, callerSignal);
172
- if (!data.accessToken || !data.refreshToken || !data.expiresAt) {
173
- throw new Error('Server returned invalid refresh credentials. Run `orizu login` again.');
174
- }
175
- const refreshed = rememberSessionCredentials({
176
- credentialType: 'session',
177
- accessToken: data.accessToken,
178
- refreshToken: data.refreshToken,
179
- expiresAt: data.expiresAt,
180
- });
181
- if (isExpired(refreshed.expiresAt)) {
182
- throw new Error('ORIZU_CREDENTIALS_REFRESH_UNUSABLE: refreshed session expires within the 30-second safety window.');
183
- }
184
- return refreshed;
185
- }
186
- async function refreshStoredCredentialsIfCurrent(baseUrl, expected, callerSignal) {
187
- return await withCredentialsTransactionLockAsync(async () => {
188
- const current = getServerCredentials(baseUrl);
189
- if (!current || !credentialsEqual(current, expected)) {
190
- return { status: 'changed', credentials: current };
191
- }
192
- const refreshed = await refreshCredentials(baseUrl, expected, callerSignal);
193
- if (!updateServerCredentialsIfCurrent(baseUrl, expected, refreshed)) {
194
- return { status: 'changed', credentials: getServerCredentials(baseUrl) };
195
- }
196
- return { status: 'refreshed', credentials: refreshed };
197
- });
198
- }
199
- function requireCurrentCredentials(baseUrl, credentials) {
200
- if (credentials)
201
- return credentials;
202
- throw new Error(`Not logged in for ${baseUrl}. Run \`orizu login --server ${baseUrl}\` (or \`--local\`) first.`);
203
- }
204
- async function refreshExpiredStoredCredentials(baseUrl, initial, callerSignal) {
205
- if (!isSessionCredentials(initial) || !isExpired(initial.expiresAt)) {
206
- return { credentials: initial, didRefresh: false };
207
- }
208
- const result = await refreshStoredCredentialsIfCurrent(baseUrl, initial, callerSignal);
209
- if (result.status === 'changed') {
210
- throw new Error('ORIZU_AUTH_CONTEXT_CHANGED: stored credentials changed before this request began; the request was not sent.');
211
- }
212
- return {
213
- credentials: requireCurrentCredentials(baseUrl, result.credentials),
214
- didRefresh: true,
215
- };
83
+ const storedSessionRequests = () => import('./stored-session-requests.js');
84
+ function notLoggedIn(baseUrl) {
85
+ return new Error(`Not logged in for ${baseUrl}. Run \`orizu login --server ${baseUrl}\` (or \`--local\`) first.`);
216
86
  }
217
87
  /**
218
88
  * Captures one authenticated server context for a multi-step operation. The
@@ -266,85 +136,41 @@ export function captureAuthenticatedRequestContext(options = {}) {
266
136
  };
267
137
  }
268
138
  const capturedCredentials = getServerCredentials(baseUrl);
269
- if (!capturedCredentials) {
270
- throw new Error(`Not logged in for ${baseUrl}. Run \`orizu login --server ${baseUrl}\` (or \`--local\`) first.`);
271
- }
272
- void getAuthorizationToken(capturedCredentials);
139
+ if (!capturedCredentials)
140
+ throw notLoggedIn(baseUrl);
141
+ rememberStoredBearers(capturedCredentials);
273
142
  let activeCredentials = capturedCredentials;
274
- async function refreshCapturedCredentials(callerSignal) {
275
- const result = await refreshStoredCredentialsIfCurrent(baseUrl, activeCredentials, callerSignal);
276
- if (result.status === 'changed') {
277
- throw new Error('ORIZU_CREDENTIALS_CHANGED: stored credentials changed before the captured session could refresh. Reconfirm the account and retry.');
278
- }
279
- activeCredentials = requireCurrentCredentials(baseUrl, result.credentials);
280
- }
143
+ const changed = 'ORIZU_CREDENTIALS_CHANGED: stored credentials changed before the captured session could refresh. Reconfirm the account and retry.';
281
144
  return {
282
145
  baseUrl,
283
146
  source: 'stored',
284
147
  async fetch(path, init = {}, onRequestStart) {
285
- let didRefresh = false;
286
- if (allowRefresh
287
- && isSessionCredentials(activeCredentials)
288
- && isExpired(activeCredentials.expiresAt)) {
289
- await refreshCapturedCredentials(init.signal);
290
- didRefresh = true;
291
- }
292
- onRequestStart?.();
293
- let response = await fetch(`${baseUrl}${path}`, {
294
- ...init,
295
- headers: {
296
- ...(init.headers || {}),
297
- Authorization: `Bearer ${getAuthorizationToken(activeCredentials)}`,
298
- },
299
- ...credentialRequestRedirectPolicy(),
300
- });
301
- if (allowRefresh
302
- && response.status === 401
303
- && isSessionCredentials(activeCredentials)
304
- && !didRefresh) {
305
- await refreshCapturedCredentials(init.signal);
306
- onRequestStart?.();
307
- response = await fetch(`${baseUrl}${path}`, {
308
- ...init,
309
- headers: {
310
- ...(init.headers || {}),
311
- Authorization: `Bearer ${getAuthorizationToken(activeCredentials)}`,
312
- },
313
- ...credentialRequestRedirectPolicy(),
314
- });
315
- }
316
- return response;
148
+ const { storedRequest } = await storedSessionRequests();
149
+ return await storedRequest({
150
+ baseUrl,
151
+ credentials: activeCredentials,
152
+ allowRefresh,
153
+ changedBeforeRequest: changed,
154
+ changedBeforeReplay: changed,
155
+ onRefreshed: refreshed => { activeCredentials = refreshed; },
156
+ }, path, init, onRequestStart);
317
157
  },
318
158
  async logout(callerSignal) {
319
- const authorizationToken = getAuthorizationToken(activeCredentials);
320
- const refreshToken = isSessionCredentials(activeCredentials)
321
- ? rememberResolvedBearer(activeCredentials.refreshToken)
322
- : null;
323
- const secrets = [authorizationToken, ...(refreshToken !== null ? [refreshToken] : [])];
324
- let remoteLogoutReason = null;
325
- try {
326
- remoteLogoutReason = await withLogoutDeadline(async (signal) => {
327
- const response = await fetch(`${baseUrl}/api/cli/auth/logout`, {
328
- method: 'POST',
329
- headers: {
330
- 'Content-Type': 'application/json',
331
- Authorization: `Bearer ${authorizationToken}`,
332
- },
333
- body: refreshToken !== null ? JSON.stringify({ refreshToken }) : undefined,
334
- ...credentialRequestRedirectPolicy(),
335
- signal,
336
- });
337
- return response.ok ? null : await describeLogoutHttpFailure(response, secrets);
338
- }, callerSignal);
339
- }
340
- catch (error) {
341
- remoteLogoutReason = describeLogoutTransportFailure(error, secrets);
342
- }
343
- const localCredentialCleared = clearServerCredentialsIfCurrent(baseUrl, activeCredentials);
344
- return { remoteError: remoteLogoutReason, localCredentialCleared };
159
+ const { storedLogout } = await storedSessionRequests();
160
+ return await storedLogout(baseUrl, baseUrl, activeCredentials, callerSignal);
345
161
  },
346
162
  };
347
163
  }
164
+ // Redacts a stored login from diagnostics from the moment it is read.
165
+ function rememberStoredBearers(credentials) {
166
+ if ('accessToken' in credentials) {
167
+ rememberResolvedBearer(credentials.accessToken);
168
+ rememberResolvedBearer(credentials.refreshToken);
169
+ }
170
+ else {
171
+ rememberResolvedBearer(credentials.apiKey);
172
+ }
173
+ }
348
174
  // The optional observer runs immediately before a resource fetch, after auth
349
175
  // preflight. Authentication refresh requests do not mark the resource as sent.
350
176
  export async function authedFetch(path, init = {}, onRequestStart) {
@@ -371,35 +197,14 @@ export async function authedFetch(path, init = {}, onRequestStart) {
371
197
  });
372
198
  }
373
199
  const credentials = getServerCredentials(baseUrl);
374
- if (!credentials) {
375
- throw new Error(`Not logged in for ${baseUrl}. Run \`orizu login --server ${baseUrl}\` (or \`--local\`) first.`);
376
- }
377
- const resolved = await refreshExpiredStoredCredentials(baseUrl, credentials, init.signal);
378
- let activeCredentials = resolved.credentials;
379
- let response = await fetchResource(`${baseUrl}${path}`, {
380
- ...init,
381
- headers: {
382
- ...(init.headers || {}),
383
- Authorization: `Bearer ${getAuthorizationToken(activeCredentials)}`,
384
- },
385
- ...credentialRequestRedirectPolicy(),
386
- });
387
- if (response.status === 401
388
- && isSessionCredentials(activeCredentials)
389
- && !resolved.didRefresh) {
390
- const refreshResult = await refreshStoredCredentialsIfCurrent(baseUrl, activeCredentials, init.signal);
391
- if (refreshResult.status === 'changed') {
392
- throw new Error('ORIZU_AUTH_CONTEXT_CHANGED: stored credentials changed after this request began; the request was not replayed.');
393
- }
394
- activeCredentials = requireCurrentCredentials(baseUrl, refreshResult.credentials);
395
- response = await fetchResource(`${baseUrl}${path}`, {
396
- ...init,
397
- headers: {
398
- ...(init.headers || {}),
399
- Authorization: `Bearer ${getAuthorizationToken(activeCredentials)}`,
400
- },
401
- ...credentialRequestRedirectPolicy(),
402
- });
403
- }
404
- return response;
200
+ if (!credentials)
201
+ throw notLoggedIn(baseUrl);
202
+ const { storedRequest } = await storedSessionRequests();
203
+ return await storedRequest({
204
+ baseUrl,
205
+ credentials,
206
+ allowRefresh: true,
207
+ changedBeforeRequest: 'ORIZU_AUTH_CONTEXT_CHANGED: stored credentials changed before this request began; the request was not sent.',
208
+ changedBeforeReplay: 'ORIZU_AUTH_CONTEXT_CHANGED: stored credentials changed after this request began; the request was not replayed.',
209
+ }, path, init, onRequestStart);
405
210
  }
@@ -1 +1 @@
1
- {"version":3,"file":"keyboard-select.d.ts","sourceRoot":"","sources":["../src/keyboard-select.ts"],"names":[],"mappings":"AAUA,UAAU,qBAAqB,CAAC,CAAC;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAOD,wBAAsB,oBAAoB,CAAC,CAAC,EAC1C,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,CAAC,EAAE,EACV,KAAK,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,EACzC,OAAO,CAAC,EAAE,qBAAqB,CAAC,CAAC,CAAC,GACjC,OAAO,CAAC,CAAC,CAAC,CAuEZ"}
1
+ {"version":3,"file":"keyboard-select.d.ts","sourceRoot":"","sources":["../src/keyboard-select.ts"],"names":[],"mappings":"AAIA,UAAU,qBAAqB,CAAC,CAAC;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,wBAAsB,oBAAoB,CAAC,CAAC,EAC1C,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,CAAC,EAAE,EACV,KAAK,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,EACzC,OAAO,CAAC,EAAE,qBAAqB,CAAC,CAAC,CAAC,GACjC,OAAO,CAAC,CAAC,CAAC,CA6CZ"}
@@ -1,4 +1,3 @@
1
- import { emitKeypressEvents } from 'readline';
2
1
  import { stdin as input, stdout as output } from 'process';
3
2
  import { sanitizeTerminalText } from './json-response.js';
4
3
  export async function promptKeyboardSelect(title, items, label, options) {
@@ -12,9 +11,6 @@ export async function promptKeyboardSelect(title, items, label, options) {
12
11
  output.write(`Use ↑/↓ to choose, Enter to confirm, or Esc to ${options?.escapeLabel || 'cancel'}.\n`);
13
12
  let selected = 0;
14
13
  let rendered = false;
15
- const rawInput = input;
16
- const wasRaw = Boolean(rawInput.isRaw);
17
- emitKeypressEvents(input);
18
14
  function render() {
19
15
  if (rendered)
20
16
  output.write(`\x1b[${items.length}A`);
@@ -26,47 +22,29 @@ export async function promptKeyboardSelect(title, items, label, options) {
26
22
  rendered = true;
27
23
  }
28
24
  render();
29
- output.write('\x1b[?25l');
30
- return new Promise((resolvePromise, rejectPromise) => {
31
- function cleanup() {
32
- input.off('keypress', onKeypress);
33
- if (rawInput.setRawMode && !wasRaw)
34
- rawInput.setRawMode(false);
35
- input.pause();
36
- output.write('\x1b[?25h\n');
37
- }
38
- function onKeypress(_chunk, key) {
39
- if (key.ctrl && key.name === 'c') {
40
- cleanup();
41
- rejectPromise(new Error('Setup cancelled.'));
42
- return;
43
- }
44
- if (key.name === 'escape') {
45
- cleanup();
46
- if (options?.onEscape)
47
- resolvePromise(options.onEscape());
48
- else
49
- rejectPromise(new Error('Setup cancelled.'));
50
- return;
51
- }
52
- if (key.name === 'up') {
53
- selected = (selected - 1 + items.length) % items.length;
54
- render();
55
- return;
56
- }
57
- if (key.name === 'down') {
58
- selected = (selected + 1) % items.length;
25
+ const onEscape = options?.onEscape;
26
+ const { readKeys } = await import('./terminal-key-session.js');
27
+ let choice;
28
+ try {
29
+ choice = await readKeys(key => {
30
+ if (key.name === 'escape')
31
+ return onEscape ? { kind: 'done', value: { escaped: true } } : { kind: 'cancel' };
32
+ if (key.name === 'up' || key.name === 'down') {
33
+ selected = (selected + (key.name === 'up' ? -1 : 1) + items.length) % items.length;
59
34
  render();
60
- return;
35
+ return { kind: 'wait' };
61
36
  }
62
- if (key.name === 'return' || key.name === 'enter') {
63
- const item = items[selected];
64
- cleanup();
65
- resolvePromise(item);
66
- }
67
- }
68
- input.on('keypress', onKeypress);
69
- input.resume();
70
- rawInput.setRawMode?.(true);
71
- });
37
+ if (key.name === 'return' || key.name === 'enter')
38
+ return { kind: 'done', value: { item: items[selected] } };
39
+ return { kind: 'wait' };
40
+ }, { hideCursor: true });
41
+ }
42
+ finally {
43
+ output.write('\n');
44
+ }
45
+ if ('item' in choice)
46
+ return choice.item;
47
+ if (!onEscape)
48
+ throw new Error('Setup cancelled.');
49
+ return onEscape();
72
50
  }
@@ -0,0 +1,18 @@
1
+ /** Sends one request; calls `onRequestStart` just before anything goes out. */
2
+ export type ProjectCreateRequest = (path: string, init: RequestInit, onRequestStart?: () => void) => Promise<Response>;
3
+ export interface CreatedProject {
4
+ id: string;
5
+ name: string;
6
+ slug: string;
7
+ teamId: string;
8
+ teamName: string;
9
+ teamSlug: string;
10
+ role: string;
11
+ }
12
+ /**
13
+ * `orizu projects create` and setup's project create: one request with a time
14
+ * limit, never retried, since a retry after an unclear outcome could make a
15
+ * second project. When the outcome is unclear, the error says how to check.
16
+ */
17
+ export declare function createProjectWithDeadline(teamSlug: string, name: string, request: ProjectCreateRequest): Promise<CreatedProject>;
18
+ //# sourceMappingURL=project-create-workflow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-create-workflow.d.ts","sourceRoot":"","sources":["../src/project-create-workflow.ts"],"names":[],"mappings":"AASA,+EAA+E;AAC/E,MAAM,MAAM,oBAAoB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,cAAc,CAAC,EAAE,MAAM,IAAI,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;AActH,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;CACb;AAmDD;;;;GAIG;AACH,wBAAsB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,cAAc,CAAC,CAkDtI"}
@@ -0,0 +1,121 @@
1
+ import { Cause, Effect, Exit, Result, Schema } from 'effect';
2
+ import { datasetTransferTimeoutMs } from './dataset-transfer-policy.js';
3
+ import { extractErrorMessage } from './error-response.js';
4
+ import { appErrorOf, unreachedCode } from './request-outcome.js';
5
+ const CreateReply = Schema.Struct({
6
+ project: Schema.Struct({
7
+ id: Schema.NonEmptyString,
8
+ name: Schema.String,
9
+ slug: Schema.NonEmptyString,
10
+ teamSlug: Schema.NonEmptyString,
11
+ teamId: Schema.optional(Schema.String),
12
+ teamName: Schema.optional(Schema.String),
13
+ role: Schema.optional(Schema.String),
14
+ }),
15
+ });
16
+ /**
17
+ * Runs one request under the time limit. `run` gets the signal to pass on and
18
+ * the hook that marks the request as sent; the body is read inside `run`, so a
19
+ * reply that stalls part way is under the limit too.
20
+ */
21
+ function withDeadline(timeoutMs, run, onFailure) {
22
+ return Effect.suspend(() => {
23
+ let sent = false;
24
+ let timedOut = false;
25
+ return Effect.tryPromise({
26
+ try: async (signal) => {
27
+ const controller = new AbortController();
28
+ const abort = () => controller.abort();
29
+ const timer = setTimeout(() => { timedOut = true; controller.abort(); }, timeoutMs);
30
+ if (signal.aborted)
31
+ abort();
32
+ else
33
+ signal.addEventListener('abort', abort, { once: true });
34
+ try {
35
+ return await run(controller.signal, () => { sent = true; });
36
+ }
37
+ finally {
38
+ clearTimeout(timer);
39
+ signal.removeEventListener('abort', abort);
40
+ controller.abort();
41
+ }
42
+ },
43
+ catch: cause => onFailure(cause, { timedOut, sent }),
44
+ });
45
+ });
46
+ }
47
+ async function runToPromise(effect) {
48
+ const exit = await Effect.runPromiseExit(effect);
49
+ if (Exit.isSuccess(exit))
50
+ return exit.value;
51
+ throw Cause.squash(exit.cause);
52
+ }
53
+ const FAILED = 'Failed to create project';
54
+ const projectMayBeCreated = (teamSlug, name) => `The project may already be created: run \`orizu projects list --team ${teamSlug}\` and look for "${name}" before creating it again.`;
55
+ class ProjectCreateFailure extends Error {
56
+ mayBeCreated;
57
+ _tag = 'ProjectCreateFailure';
58
+ constructor(message, mayBeCreated) {
59
+ super(message);
60
+ this.mayBeCreated = mayBeCreated;
61
+ }
62
+ }
63
+ /**
64
+ * `orizu projects create` and setup's project create: one request with a time
65
+ * limit, never retried, since a retry after an unclear outcome could make a
66
+ * second project. When the outcome is unclear, the error says how to check.
67
+ */
68
+ export async function createProjectWithDeadline(teamSlug, name, request) {
69
+ const create = withDeadline(datasetTransferTimeoutMs(), async (signal, markSent) => {
70
+ const response = await request('/api/cli/projects', {
71
+ method: 'POST',
72
+ headers: { 'Content-Type': 'application/json' },
73
+ body: JSON.stringify({ teamSlug, name }),
74
+ signal,
75
+ }, markSent);
76
+ if (!response.ok) {
77
+ const appError = await appErrorOf(response.clone());
78
+ const detail = await extractErrorMessage(response);
79
+ // A 5xx that is not the app's own error may come from a gateway that gave up while the app kept working.
80
+ if (response.status < 400 || (response.status >= 500 && appError === null)) {
81
+ throw new ProjectCreateFailure(`Project create got no clear answer (status ${response.status}).`, true);
82
+ }
83
+ throw new ProjectCreateFailure(detail.startsWith(FAILED) ? detail : `${FAILED}: ${detail}`, false);
84
+ }
85
+ const body = await response.text();
86
+ let data;
87
+ try {
88
+ data = JSON.parse(body);
89
+ }
90
+ catch {
91
+ data = undefined;
92
+ }
93
+ const decoded = Schema.decodeUnknownResult(CreateReply)(data);
94
+ if (Result.isFailure(decoded)) {
95
+ throw new ProjectCreateFailure(`Project create returned an unexpected response (status ${response.status}).`, true);
96
+ }
97
+ const project = decoded.success.project;
98
+ return {
99
+ id: project.id,
100
+ name: project.name,
101
+ slug: project.slug,
102
+ teamId: project.teamId || '',
103
+ teamName: project.teamName || project.teamSlug,
104
+ teamSlug: project.teamSlug,
105
+ role: project.role || 'admin',
106
+ };
107
+ }, (cause, { timedOut, sent }) => {
108
+ if (cause instanceof ProjectCreateFailure)
109
+ return cause;
110
+ if (timedOut)
111
+ return new ProjectCreateFailure('Project create exceeded its deadline.', sent);
112
+ const unreached = unreachedCode(cause);
113
+ if (unreached)
114
+ return new ProjectCreateFailure(`${FAILED}: could not reach the server (${unreached}).`, false);
115
+ const message = cause instanceof Error ? cause.message : String(cause);
116
+ if (!sent || message.startsWith('ORIZU_AUTH_CONTEXT_CHANGED:'))
117
+ return new ProjectCreateFailure(message, false);
118
+ return new ProjectCreateFailure(`Project create did not finish: ${message}`, true);
119
+ });
120
+ return await runToPromise(create.pipe(Effect.mapError(failure => new Error(failure.mayBeCreated ? `${failure.message}\n${projectMayBeCreated(teamSlug, name)}` : failure.message))));
121
+ }
@@ -0,0 +1,8 @@
1
+ import { type GlobalInstall, type SelfUpdateIo } from './self-update.js';
2
+ /**
3
+ * Runs the update with stop handlers installed for exactly its duration. A
4
+ * stop interrupts it: the running child gets the same signal and the stop cap
5
+ * to exit, the command says what is installed now, then ends by that signal.
6
+ */
7
+ export declare function runSelfUpdate(io: SelfUpdateIo, install: GlobalInstall): Promise<number>;
8
+ //# sourceMappingURL=self-update-command.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"self-update-command.d.ts","sourceRoot":"","sources":["../src/self-update-command.ts"],"names":[],"mappings":"AAYA,OAAO,EAQL,KAAK,aAAa,EAClB,KAAK,YAAY,EAClB,MAAM,kBAAkB,CAAA;AAyWzB;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,EAAE,EAAE,YAAY,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CA8B7F"}