@kamilio/wire 0.1.3 → 0.1.5

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 (48) hide show
  1. package/README.md +57 -18
  2. package/dist/auth.d.ts.map +1 -1
  3. package/dist/auth.js +3 -10
  4. package/dist/auth.js.map +1 -1
  5. package/dist/executable.d.ts.map +1 -1
  6. package/dist/executable.js +33 -8
  7. package/dist/executable.js.map +1 -1
  8. package/dist/hooks.d.ts.map +1 -1
  9. package/dist/hooks.js +18 -7
  10. package/dist/hooks.js.map +1 -1
  11. package/node_modules/provider-chatgpt/dist/chatgpt.d.ts.map +1 -1
  12. package/node_modules/provider-chatgpt/dist/chatgpt.js +22 -3
  13. package/node_modules/provider-chatgpt/dist/chatgpt.js.map +1 -1
  14. package/node_modules/provider-chatgpt/src/chatgpt.ts +20 -3
  15. package/node_modules/provider-gmail/dist/gmail.d.ts.map +1 -1
  16. package/node_modules/provider-gmail/dist/gmail.js +2 -0
  17. package/node_modules/provider-gmail/dist/gmail.js.map +1 -1
  18. package/node_modules/provider-gmail/src/gmail.ts +1 -0
  19. package/node_modules/provider-google-docs/dist/google-docs.d.ts.map +1 -1
  20. package/node_modules/provider-google-docs/dist/google-docs.js +9 -6
  21. package/node_modules/provider-google-docs/dist/google-docs.js.map +1 -1
  22. package/node_modules/provider-google-docs/src/google-docs.ts +10 -7
  23. package/node_modules/provider-notion/dist/notion-sync.d.ts +1 -1
  24. package/node_modules/provider-notion/dist/notion-sync.d.ts.map +1 -1
  25. package/node_modules/provider-notion/dist/notion-sync.js +41 -12
  26. package/node_modules/provider-notion/dist/notion-sync.js.map +1 -1
  27. package/node_modules/provider-notion/dist/notion.js +1 -1
  28. package/node_modules/provider-notion/dist/notion.js.map +1 -1
  29. package/node_modules/provider-notion/src/notion-sync.ts +41 -12
  30. package/node_modules/provider-notion/src/notion.ts +1 -1
  31. package/node_modules/wire-core/dist/core/transform.d.ts +1 -0
  32. package/node_modules/wire-core/dist/core/transform.d.ts.map +1 -1
  33. package/node_modules/wire-core/dist/core/transform.js +4 -0
  34. package/node_modules/wire-core/dist/core/transform.js.map +1 -1
  35. package/node_modules/wire-core/dist/operations.d.ts.map +1 -1
  36. package/node_modules/wire-core/dist/operations.js +8 -6
  37. package/node_modules/wire-core/dist/operations.js.map +1 -1
  38. package/node_modules/wire-core/dist/runtime/chrome.d.ts +1 -0
  39. package/node_modules/wire-core/dist/runtime/chrome.d.ts.map +1 -1
  40. package/node_modules/wire-core/dist/runtime/chrome.js +64 -79
  41. package/node_modules/wire-core/dist/runtime/chrome.js.map +1 -1
  42. package/node_modules/wire-core/src/core/transform.ts +5 -1
  43. package/node_modules/wire-core/src/operations.ts +8 -6
  44. package/node_modules/wire-core/src/runtime/chrome.ts +69 -75
  45. package/package.json +3 -3
  46. package/src/auth.ts +3 -9
  47. package/src/executable.ts +33 -8
  48. package/src/hooks.ts +16 -6
@@ -28,6 +28,8 @@ export async function chromeLaunchArguments(environment: NodeEnvironment, startU
28
28
  ]);
29
29
  }
30
30
 
31
+ const chromeExecutable = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
32
+
31
33
  interface ChromeCookie {
32
34
  readonly domain: string;
33
35
  readonly expires: number;
@@ -36,6 +38,10 @@ interface ChromeCookie {
36
38
  readonly path: string;
37
39
  readonly secure: boolean;
38
40
  readonly value: string;
41
+ readonly sameSite?: string;
42
+ readonly sourceScheme?: string;
43
+ readonly sourcePort?: number;
44
+ readonly partitionKey?: Readonly<Record<string, unknown>>;
39
45
  }
40
46
 
41
47
  interface PendingRequest {
@@ -133,7 +139,7 @@ function devtoolsUrl(stderr: NodeJS.ReadableStream): Promise<string> {
133
139
  const match = /DevTools listening on (ws:\/\/[^\s]+)/.exec(contents);
134
140
  if (match !== null) resolve(match[1]!);
135
141
  });
136
- stderr.on("end", () => reject(new Error(contents)));
142
+ stderr.on("end", () => reject(new Error(contents === "" ? "Chrome exited before DevTools started. A Wire Chrome window is already open from a previous login; close it and retry." : contents)));
137
143
  });
138
144
  }
139
145
 
@@ -155,54 +161,6 @@ async function pageDevtoolsUrl(browserUrl: string, domains: readonly string[]):
155
161
  return targets.find((target) => target.type === "page" && domains.some((domain) => new URL(target.url).hostname === domain || new URL(target.url).hostname.endsWith(`.${domain}`)))!.webSocketDebuggerUrl;
156
162
  }
157
163
 
158
- async function chatgptBrowserMetadata(pageConnection: ChromeConnection): Promise<Readonly<Record<string, string>> | null> {
159
- const evaluation = await pageConnection.request("Runtime.evaluate", {
160
- expression: `new Promise((resolve) => {
161
- (async () => {
162
- if (location.hostname !== "chatgpt.com") {
163
- resolve({ ok: false });
164
- return;
165
- }
166
- const response = await fetch("/api/auth/session", { cache: "no-store" });
167
- const text = await response.text();
168
- if (!text.startsWith("{")) {
169
- resolve({ ok: false });
170
- return;
171
- }
172
- const session = JSON.parse(text);
173
- if (!("account" in session) || !("id" in session.account) || !("accessToken" in session) || "error" in session) {
174
- resolve({ ok: false });
175
- return;
176
- }
177
- const backend = await fetch("/backend-api/conversations?offset=0&limit=1&order=updated&is_archived=false&is_starred=false", {
178
- cache: "no-store",
179
- headers: {
180
- authorization: \`Bearer \${session.accessToken}\`,
181
- "chatgpt-account-id": session.account.id
182
- }
183
- });
184
- resolve({ ok: backend.ok, account_id: session.account.id });
185
- })().catch(() => resolve({ ok: false }));
186
- })`,
187
- awaitPromise: true,
188
- returnByValue: true,
189
- }) as { result: { value?: { ok?: boolean; account_id?: string } } };
190
- const value = evaluation.result.value!;
191
- if (value.ok !== true) return null;
192
- return Object.freeze({ account_id: value.account_id! });
193
- }
194
-
195
- async function chatgptChallengeUrl(pageConnection: ChromeConnection): Promise<string | null> {
196
- const evaluation = await pageConnection.request("Runtime.evaluate", {
197
- expression: `(() => ({ href: location.href, title: document.title, text: document.body.innerText }))()`,
198
- returnByValue: true,
199
- }) as { result: { value?: { href: string; title: string; text: string } } };
200
- const value = evaluation.result.value!;
201
- const pageText = `${value.href}\n${value.title}\n${value.text}`;
202
- if (/cdn-cgi\/challenge-platform|cf_chl|Just a moment|Verify you are human|Cloudflare/i.test(pageText)) return value.href;
203
- return null;
204
- }
205
-
206
164
  function chromeCookies(values: readonly ChromeCookie[], domains: readonly string[]): readonly Cookie[] {
207
165
  return Object.freeze(values.filter((cookie) => domains.some((domain) => cookie.domain === domain || cookie.domain.endsWith(`.${domain}`))).map((cookie) => Object.freeze({
208
166
  domain: cookie.domain,
@@ -222,6 +180,7 @@ export interface ChromeCookieExtraction {
222
180
  readonly domains: readonly string[];
223
181
  readonly ready: (cookies: readonly Cookie[]) => boolean;
224
182
  readonly metadataExpression?: string;
183
+ readonly freshSession?: true;
225
184
  readonly verify: (cookies: readonly Cookie[], metadata: Readonly<Record<string, string>>) => Promise<boolean>;
226
185
  }
227
186
 
@@ -242,10 +201,66 @@ function sleep(milliseconds: number): Promise<void> {
242
201
  return new Promise((resolve) => setTimeout(resolve, milliseconds));
243
202
  }
244
203
 
204
+ async function headlessChromeConnection(environment: NodeEnvironment): Promise<Readonly<{ chrome: ReturnType<typeof spawn>; connection: ChromeConnection }>> {
205
+ const chrome = spawn(chromeExecutable, [`--user-data-dir=${chromeUserDataDir(environment)}`, "--remote-debugging-port=0", "--headless=new", "--no-first-run", "--no-default-browser-check", "about:blank"], { detached: true, stdio: ["ignore", "ignore", "pipe"] });
206
+ const connection = new ChromeConnection(await devtoolsUrl(chrome.stderr!));
207
+ await connection.opened();
208
+ return Object.freeze({ chrome, connection });
209
+ }
210
+
211
+ async function expireServiceCookies(connection: ChromeConnection, domains: readonly string[]): Promise<void> {
212
+ const serviceDomain = (domain: string) => { const bare = domain.startsWith(".") ? domain.slice(1) : domain; return domains.some((value) => bare === value || bare.endsWith(`.${value}`)); };
213
+ const stored = await connection.request("Storage.getCookies") as { cookies: readonly ChromeCookie[] };
214
+ const expiredCopies = stored.cookies.filter((cookie) => serviceDomain(cookie.domain)).map((cookie) => ({
215
+ name: cookie.name,
216
+ value: "",
217
+ domain: cookie.domain,
218
+ path: cookie.path,
219
+ secure: cookie.secure,
220
+ httpOnly: cookie.httpOnly,
221
+ expires: 1,
222
+ ...(cookie.sameSite === undefined ? {} : { sameSite: cookie.sameSite }),
223
+ ...(cookie.sourceScheme === undefined ? {} : { sourceScheme: cookie.sourceScheme }),
224
+ ...(cookie.sourcePort === undefined ? {} : { sourcePort: cookie.sourcePort }),
225
+ ...(cookie.partitionKey === undefined ? {} : { partitionKey: cookie.partitionKey }),
226
+ }));
227
+ if (expiredCopies.length > 0) await connection.request("Storage.setCookies", { cookies: expiredCopies });
228
+ }
229
+
230
+ async function extractDetachedChromeCookies(environment: NodeEnvironment, extraction: ChromeCookieExtraction): Promise<ChromeCookieResult> {
231
+ await mkdir(chromeUserDataDir(environment), { recursive: true });
232
+ const prep = await headlessChromeConnection(environment);
233
+ await expireServiceCookies(prep.connection, extraction.domains);
234
+ await closeChrome(prep.chrome, prep.connection);
235
+ const chrome = spawn(chromeExecutable, [`--user-data-dir=${chromeUserDataDir(environment)}`, "--no-first-run", "--no-default-browser-check", extraction.startUrl], { detached: true, stdio: ["ignore", "ignore", "ignore"] });
236
+ stderr.write(`\nLog in to ${extraction.service} in the opened Chrome window, then quit Chrome (Cmd+Q). Wire picks up the session after Chrome exits.\n`);
237
+ let interrupted = false;
238
+ const interrupt = () => {
239
+ interrupted = true;
240
+ process.kill(-chrome.pid!, "SIGTERM");
241
+ };
242
+ process.on("SIGINT", interrupt);
243
+ await new Promise((resolve) => chrome.once("exit", resolve));
244
+ process.off("SIGINT", interrupt);
245
+ await sleep(500);
246
+ const harvest = await headlessChromeConnection(environment);
247
+ const stored = await harvest.connection.request("Storage.getCookies") as { cookies: readonly ChromeCookie[] };
248
+ await closeChrome(harvest.chrome, harvest.connection);
249
+ const cookies = chromeCookies(stored.cookies, extraction.domains);
250
+ if (interrupted) {
251
+ const save = await confirmSaveLogin();
252
+ if (!save) throw new Error("Login not saved");
253
+ return Object.freeze({ cookies, metadata: Object.freeze({}), manual: true });
254
+ }
255
+ if (!extraction.ready(cookies)) throw new Error(`${extraction.service} login did not complete: the Chrome window was quit before login finished.`);
256
+ return Object.freeze({ cookies, metadata: Object.freeze({}) });
257
+ }
258
+
245
259
  export async function extractChromeCookies(environment: NodeEnvironment, extraction: ChromeCookieExtraction): Promise<ChromeCookieResult> {
260
+ if (extraction.freshSession === true) return extractDetachedChromeCookies(environment, extraction);
246
261
  const profile = chromeUserDataDir(environment);
247
262
  await mkdir(profile, { recursive: true });
248
- const chrome = spawn("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", [...await chromeLaunchArguments(environment, extraction.startUrl)], { detached: true, stdio: ["ignore", "ignore", "pipe"] });
263
+ const chrome = spawn(chromeExecutable, [...await chromeLaunchArguments(environment, extraction.startUrl)], { detached: true, stdio: ["ignore", "ignore", "pipe"] });
249
264
  let interrupted = false;
250
265
  let interrupting = false;
251
266
  let interruptResult: ChromeCookieResult | null | undefined;
@@ -268,7 +283,7 @@ export async function extractChromeCookies(environment: NodeEnvironment, extract
268
283
  const browserUrl = await devtoolsUrl(chrome.stderr!);
269
284
  const connection = new ChromeConnection(browserUrl);
270
285
  await connection.opened();
271
- const pageConnection = extraction.metadataExpression === undefined && extraction.service !== "chatgpt" ? undefined : new ChromeConnection(await pageDevtoolsUrl(browserUrl, extraction.domains));
286
+ const pageConnection = extraction.metadataExpression === undefined ? undefined : new ChromeConnection(await pageDevtoolsUrl(browserUrl, extraction.domains));
272
287
  if (pageConnection !== undefined) await pageConnection.opened();
273
288
  if (pageConnection !== undefined) await Promise.race([sleep(2000), interruptPromise]);
274
289
  const currentResult = async (): Promise<ChromeCookieResult> => {
@@ -291,33 +306,12 @@ export async function extractChromeCookies(environment: NodeEnvironment, extract
291
306
  return interruptResult;
292
307
  };
293
308
  for (;;) {
294
- if (extraction.service === "chatgpt") {
295
- const challengeUrl = await chatgptChallengeUrl(pageConnection!);
296
- const manualResult = await interruptedResult();
297
- if (manualResult !== undefined) return manualResult;
298
- if (challengeUrl !== null) {
299
- await closeChrome(chrome, connection, pageConnection);
300
- process.off("SIGINT", interrupt);
301
- throw new Error(`ChatGPT login is stuck on an HTML challenge: ${challengeUrl}`);
302
- }
303
- }
304
309
  const result = await currentResult();
305
310
  const manualResult = await interruptedResult();
306
311
  if (manualResult !== undefined) return manualResult;
307
312
  if (extraction.ready(result.cookies)) {
308
- let metadata: Readonly<Record<string, string>> = result.metadata;
309
- if (extraction.service === "chatgpt") {
310
- const chatgptMetadata = await Promise.race([chatgptBrowserMetadata(pageConnection!), interruptPromise.then(() => undefined)]);
311
- const interruptedChatgptResult = await interruptedResult();
312
- if (interruptedChatgptResult !== undefined) return interruptedChatgptResult;
313
- if (chatgptMetadata === undefined) continue;
314
- if (chatgptMetadata === null) {
315
- await closeChrome(chrome, connection, pageConnection);
316
- process.off("SIGINT", interrupt);
317
- throw new Error("ChatGPT login is blocked by an HTML challenge. Complete `wire chatgpt login` in the opened Chrome window, then retry.");
318
- }
319
- metadata = chatgptMetadata;
320
- } else if (extraction.metadataExpression !== undefined) {
313
+ const metadata: Readonly<Record<string, string>> = result.metadata;
314
+ if (extraction.metadataExpression !== undefined) {
321
315
  if (Object.keys(metadata).length === 0) {
322
316
  await Promise.race([sleep(1000), interruptPromise]);
323
317
  const interruptedMetadataResult = await interruptedResult();
@@ -325,7 +319,7 @@ export async function extractChromeCookies(environment: NodeEnvironment, extract
325
319
  continue;
326
320
  }
327
321
  }
328
- if (extraction.service !== "chatgpt" && !(await Promise.race([extraction.verify(result.cookies, metadata), interruptPromise.then(() => false)]))) {
322
+ if (!(await Promise.race([extraction.verify(result.cookies, metadata), interruptPromise.then(() => false)]))) {
329
323
  const interruptedVerifyResult = await interruptedResult();
330
324
  if (interruptedVerifyResult !== undefined) return interruptedVerifyResult;
331
325
  await Promise.race([sleep(1000), interruptPromise]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kamilio/wire",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Attach Notion, Google Docs, Slack, Gmail, Asana, ChatGPT, and Zoom resources as local Markdown you can sync.",
5
5
  "keywords": [
6
6
  "markdown",
@@ -87,8 +87,8 @@
87
87
  "scripts": {
88
88
  "build": "tsc -p tsconfig.json",
89
89
  "dev-link": "npm link",
90
- "prepack": "rm -rf node_modules && mkdir -p node_modules && for package in provider-asana provider-chatgpt provider-gmail provider-google-docs provider-notion provider-slack provider-zoom wire-core; do cp -R ../$package node_modules/$package; done && for package in ajv commander fast-deep-equal fast-string-truncated-width fast-string-width fast-wrap-ansi fast-uri ignore jose jsonc-parser json-schema-traverse pct-encode require-from-string sisteransi smol-toml tiny-stdio-mcp-server toolcraft toolcraft-schema uri-template uri-template-lite yaml; do cp -R ../../node_modules/$package node_modules/$package; done",
91
- "postpack": "rm -rf node_modules",
90
+ "prepack": "cp ../../README.md README.md && rm -rf node_modules && mkdir -p node_modules && for package in provider-asana provider-chatgpt provider-gmail provider-google-docs provider-notion provider-slack provider-zoom wire-core; do cp -R ../$package node_modules/$package; done && for package in ajv commander fast-deep-equal fast-string-truncated-width fast-string-width fast-wrap-ansi fast-uri ignore jose jsonc-parser json-schema-traverse pct-encode require-from-string sisteransi smol-toml tiny-stdio-mcp-server toolcraft toolcraft-schema uri-template uri-template-lite yaml; do cp -R ../../node_modules/$package node_modules/$package; done",
91
+ "postpack": "rm -rf node_modules README.md",
92
92
  "test": "node --test --test-concurrency=1 test/**/*.test.mjs",
93
93
  "typecheck": "tsc -p tsconfig.json --noEmit",
94
94
  "check": "npm run typecheck && npm run build && npm test"
package/src/auth.ts CHANGED
@@ -267,19 +267,13 @@ export function composeAuth(runtime: RuntimeCapabilities, environment: NodeEnvir
267
267
  await saveCookies(service, state.cookies, state.metadata);
268
268
  return state.result;
269
269
  };
270
- const extract = async (service: AuthService, startUrl: string, domains: readonly string[], ready: (values: readonly Cookie[]) => boolean, metadataExpression?: string) => {
271
- const extraction = { service, startUrl, domains, ready, verify: async (values: readonly Cookie[], metadata: Readonly<Record<string, string>>) => (await verifyCookieState(service, values, metadata)).result !== null, ...(metadataExpression === undefined ? {} : { metadataExpression }) };
270
+ const extract = async (service: AuthService, startUrl: string, domains: readonly string[], ready: (values: readonly Cookie[]) => boolean, metadataExpression?: string, freshSession?: true) => {
271
+ const extraction = { service, startUrl, domains, ready, verify: async (values: readonly Cookie[], metadata: Readonly<Record<string, string>>) => (await verifyCookieState(service, values, metadata)).result !== null, ...(metadataExpression === undefined ? {} : { metadataExpression }), ...(freshSession === undefined ? {} : { freshSession }) };
272
272
  const result = await extractCookies(environment, extraction);
273
273
  if (result.manual === true) {
274
274
  await saveCookies(service, result.cookies, result.metadata);
275
275
  return Object.freeze({ service, identity: Object.freeze({ saved: true }) });
276
276
  }
277
- if (service === "chatgpt") {
278
- await saveCookies(service, result.cookies, result.metadata);
279
- const accountId = result.metadata["account_id"];
280
- if (accountId === undefined) throw new Error("chatgpt cookie authentication failed. Run `wire chatgpt login` once; other commands reuse saved cookies.");
281
- return Object.freeze({ service, identity: Object.freeze({ account_id: accountId }) });
282
- }
283
277
  const verified = await verifyCookieState(service, result.cookies, result.metadata);
284
278
  if (verified.result === null) throw new Error(`${service} cookie authentication failed. Run \`wire ${service} login\` once; other commands reuse saved cookies.`);
285
279
  await saveCookies(service, verified.cookies, verified.metadata);
@@ -293,7 +287,7 @@ export function composeAuth(runtime: RuntimeCapabilities, environment: NodeEnvir
293
287
  return Object.freeze({ service, deleted: true });
294
288
  },
295
289
  extractAsana: () => extract("asana", "https://app.asana.com/", ["asana.com"], (values) => values.some((cookie) => cookie.domain === ".asana.com" || cookie.domain.endsWith(".asana.com"))),
296
- extractChatgpt: () => extract("chatgpt", "https://chatgpt.com/", ["chatgpt.com", "openai.com"], requiredReady(["oai-did", "__Secure-next-auth.session-token"])),
290
+ extractChatgpt: () => extract("chatgpt", "https://chatgpt.com/", ["chatgpt.com", "openai.com"], (values) => values.some((cookie) => cookie.name === "oai-did") && values.some((cookie) => cookie.name === "__Secure-next-auth.session-token" || cookie.name === "__Secure-next-auth.session-token.0"), undefined, true),
297
291
  extractGmail: () => extract("gmail", "https://mail.google.com/mail/u/0/", ["google.com"], googleReady),
298
292
  extractGoogleDocs: () => extract("google-docs", "https://docs.google.com/", ["google.com"], googleReady),
299
293
  extractNotion: () => extract("notion", "https://www.notion.so/login", ["notion.so", "notion.com"], requiredReady(["token_v2", "notion_user_id", "notion_users"])),
package/src/executable.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync, statSync } from "node:fs";
2
2
  import { writeFile } from "node:fs/promises";
3
- import { dirname, join } from "node:path";
3
+ import { dirname, isAbsolute, join, resolve } from "node:path";
4
4
 
5
5
  import { createRoot } from "./adapters/root.js";
6
6
  import { composeAuth } from "./auth.js";
@@ -52,15 +52,21 @@ function loadConfiguredEnvironment(environment: NodeEnvironment, currentDirector
52
52
  return config.env === undefined ? environment : Object.freeze({ ...environment, ...config.env });
53
53
  }
54
54
 
55
+ function operationPath(value: string, path: string): string {
56
+ if (value.startsWith("http://") || value.startsWith("https://")) return path;
57
+ return isAbsolute(value) ? value : resolve(path, value);
58
+ }
59
+
55
60
  export function createExecutableRoot(environment: NodeEnvironment, currentDirectory: string, options = { allowPaste: true }) {
56
61
  const repositoryRoot = environment["WIRE_REPOSITORY_ROOT"] ?? discoverRepositoryRoot(currentDirectory);
57
62
  const baseEnvironment = repositoryRoot === undefined ? environment : Object.freeze({ ...environment, WIRE_REPOSITORY_ROOT: repositoryRoot });
58
- const resolvedEnvironment = loadConfiguredEnvironment(baseEnvironment, currentDirectory);
63
+ let activeConfigurationPath = currentDirectory;
64
+ const configuredEnvironment = () => loadConfiguredEnvironment(baseEnvironment, activeConfigurationPath);
59
65
  const http = createNodeHttp();
60
66
  const filesystem = createNodeFilesystem();
61
67
  const processCapability = createNodeProcess();
62
68
  const clock = createNodeClock();
63
- const configuration = createNodeConfiguration(resolvedEnvironment);
69
+ const configuration = Object.freeze({ get: (name: string) => createNodeConfiguration(configuredEnvironment()).get(name) });
64
70
  const home = configuration.get("HOME");
65
71
  const runtimeBase = {
66
72
  http,
@@ -69,14 +75,14 @@ export function createExecutableRoot(environment: NodeEnvironment, currentDirect
69
75
  clock,
70
76
  openFiles: createNodeOpenFiles(processCapability),
71
77
  configuration,
72
- secrets: createNodeSecrets(filesystem, resolvedEnvironment),
78
+ secrets: createNodeSecrets(filesystem, configuredEnvironment()),
73
79
  gmailTokens: Object.freeze({ load: () => createGoogleTokensCapability(filesystem, http, clock, configuration.get("GOOGLE_CREDENTIALS_FILE"), configuration.get("GOOGLE_TOKEN_FILE")).load(), refresh: () => createGoogleTokensCapability(filesystem, http, clock, configuration.get("GOOGLE_CREDENTIALS_FILE"), configuration.get("GOOGLE_TOKEN_FILE")).refresh() }),
74
80
  googleFormsTokens: Object.freeze({ load: () => createGoogleTokensCapability(filesystem, http, clock, configuration.get("GOOGLE_CREDENTIALS_FILE"), configuration.get("GOOGLE_FORMS_TOKEN_FILE")).load(), refresh: () => createGoogleTokensCapability(filesystem, http, clock, configuration.get("GOOGLE_CREDENTIALS_FILE"), configuration.get("GOOGLE_FORMS_TOKEN_FILE")).refresh() }),
75
81
  };
76
82
  let auth: ReturnType<typeof composeAuth>;
77
- const runtime = Object.freeze({ ...runtimeBase, cookies: createCookiesCapability(filesystem, () => home, () => resolvedEnvironment["WIRE_REPOSITORY_ROOT"]) });
78
- auth = composeAuth(runtime, resolvedEnvironment, extractChromeCookies);
79
- const wire = withWireHooks(composeWire({
83
+ const runtime = Object.freeze({ ...runtimeBase, cookies: createCookiesCapability(filesystem, () => home, () => configuredEnvironment()["WIRE_REPOSITORY_ROOT"]) });
84
+ auth = composeAuth(runtime, configuredEnvironment(), extractChromeCookies);
85
+ const baseWire = composeWire({
80
86
  home,
81
87
  fetchInput: runtime,
82
88
  catalog: serviceCatalog,
@@ -98,6 +104,25 @@ export function createExecutableRoot(environment: NodeEnvironment, currentDirect
98
104
  watch: createNodeWatch(),
99
105
  now: runtime.clock.now,
100
106
  open: runtime.openFiles.open,
101
- }), { currentDirectory, home, environment: resolvedEnvironment });
107
+ });
108
+ const configuredWire = Object.freeze({
109
+ ...baseWire,
110
+ attach: async (url: string, path: string) => { activeConfigurationPath = path; return baseWire.attach(url, path); },
111
+ create: async (url: string, path: string) => { activeConfigurationPath = path; return baseWire.create(url, path); },
112
+ view: async (url: string) => { activeConfigurationPath = currentDirectory; return baseWire.view(url); },
113
+ downloadSource: async (url: string, path: string) => { activeConfigurationPath = path; return baseWire.downloadSource(url, path); },
114
+ sync: async (value: string, path: string) => { activeConfigurationPath = operationPath(value, path); return baseWire.sync(value, path); },
115
+ download: async (value: string, path: string) => { activeConfigurationPath = operationPath(value, path); return baseWire.download(value, path); },
116
+ detach: async (value: string, path: string) => { activeConfigurationPath = operationPath(value, path); return baseWire.detach(value, path); },
117
+ unlink: async (value: string, path: string) => { activeConfigurationPath = operationPath(value, path); return baseWire.unlink(value, path); },
118
+ watch: async (value: string, path: string) => { activeConfigurationPath = operationPath(value, path); return baseWire.watch(value, path); },
119
+ openResource: async (value: string, path: string) => { activeConfigurationPath = operationPath(value, path); return baseWire.openResource(value, path); },
120
+ syncAll: async (path: string) => { activeConfigurationPath = path; return baseWire.syncAll(path); },
121
+ listResources: async (path: string) => { activeConfigurationPath = path; return baseWire.listResources(path); },
122
+ showResource: async (value: string, path: string) => { activeConfigurationPath = operationPath(value, path); return baseWire.showResource(value, path); },
123
+ init: baseWire.init,
124
+ switchBackend: baseWire.switchBackend,
125
+ });
126
+ const wire = withWireHooks(configuredWire, { currentDirectory, home, environment: configuredEnvironment() });
102
127
  return createRoot(wire, currentDirectory, auth, readStandardInput, options);
103
128
  }
package/src/hooks.ts CHANGED
@@ -5,7 +5,7 @@ import { dirname, isAbsolute, join, resolve } from "node:path";
5
5
  import { promisify } from "node:util";
6
6
 
7
7
  import type { Resource, Wire, WireResult } from "wire-core";
8
- import { configuredWireRoot, openWireRegistry, wireRelativePath } from "wire-core";
8
+ import { configuredWireRoot, loadWireConfig, openWireRegistry, wireRelativePath } from "wire-core";
9
9
  import type { WireWatchSyncHook } from "wire-core/internal/operations";
10
10
  import { wireWatchHooks } from "wire-core/internal/operations";
11
11
 
@@ -118,10 +118,18 @@ async function updateMovedResult(options: WireHookOptions, wireRoot: string, res
118
118
  { ...primary, path: relativePath },
119
119
  ],
120
120
  };
121
- if (result.summary.action !== "detached") await (await openWireRegistry(wireRoot, options.home)).put(resource);
121
+ if (result.summary.action !== "detached") {
122
+ const registry = await openWireRegistry(wireRoot, options.home);
123
+ if ((await registry.listResources()).some((item) => item.id === result.resource.id)) await registry.put(resource);
124
+ }
122
125
  return { ...result, resource, path: outputPath, summary: { ...result.summary, local: outputPath } };
123
126
  }
124
127
 
128
+ async function hookEnvironment(options: WireHookOptions, wireRoot: string): Promise<Record<string, string>> {
129
+ const config = await loadWireConfig(wireRoot);
130
+ return stringEnvironment({ ...options.environment, ...config.env });
131
+ }
132
+
125
133
  async function runHookCommands(wireRoot: string, event: WireHookEvent, environment: Readonly<Record<string, string>>): Promise<Record<string, string>> {
126
134
  let current = { ...environment };
127
135
  for (const path of await hookCommandPaths(wireRoot, event)) {
@@ -132,10 +140,11 @@ async function runHookCommands(wireRoot: string, event: WireHookEvent, environme
132
140
  }
133
141
 
134
142
  async function runResourceHooks(options: WireHookOptions, command: string, result: WireResult): Promise<HookState> {
135
- const wireRoot = (await configuredWireRoot(result.path, options.home))!;
143
+ const wireRoot = await configuredWireRoot(result.path, options.home);
144
+ if (wireRoot === null) return { result, environment: {} };
136
145
  let currentResult = result;
137
146
  let environment = {
138
- ...stringEnvironment(options.environment),
147
+ ...await hookEnvironment(options, wireRoot),
139
148
  ...resourceEnvironment(command, options.currentDirectory, wireRoot, currentResult),
140
149
  };
141
150
  for (const path of await hookCommandPaths(wireRoot, "post-resource")) {
@@ -152,9 +161,10 @@ async function runResourceHooks(options: WireHookOptions, command: string, resul
152
161
  }
153
162
 
154
163
  async function runSummaryHooks(options: WireHookOptions, event: WireHookEvent, command: string, rootPath: string, results: readonly WireResult[], environment: Readonly<Record<string, string>>): Promise<Record<string, string>> {
155
- const wireRoot = (await configuredWireRoot(rootPath, options.home))!;
164
+ const wireRoot = await configuredWireRoot(rootPath, options.home);
165
+ if (wireRoot === null) return {};
156
166
  return runHookCommands(wireRoot, event, {
157
- ...stringEnvironment(options.environment),
167
+ ...await hookEnvironment(options, wireRoot),
158
168
  ...environment,
159
169
  ...summaryEnvironment(event, command, options.currentDirectory, wireRoot, results),
160
170
  });