xaq-cli 0.1.0-nightly.95 → 0.1.0-nightly.96

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/dist/cli.js CHANGED
@@ -59,7 +59,7 @@ const SERVER_GROUP = "Run xaq:";
59
59
  const AGENT_GROUP = "On an agent's machine:";
60
60
  withServerOptions(program.command("start", { isDefault: true }).description("run xaq in this terminal and print the setup link (the default command)").summary("run it in this terminal (the default command)").helpGroup(SERVER_GROUP)).option("--no-open", "don't open a browser on first run").option("--dev", "development: API only, the console runs on Vite (:5173)").allowExcessArguments().action(async (o, cmd) => {
61
61
  if (cmd.args.length) throw new Error(unknownCommand(cmd.args[0]));
62
- const { runForeground } = await import("./server-TeB2QMu0.js");
62
+ const { runForeground } = await import("./server-C0QWLPW3.js");
63
63
  await runForeground({
64
64
  ...configInput(o),
65
65
  dev: !!o.dev,
@@ -80,7 +80,7 @@ service.command("uninstall").description("stop and remove the service (keeps you
80
80
  });
81
81
  service.command("update-run", { hidden: true }).requiredOption("-d, --data <dir>", "the service's data directory").requiredOption("--id <id>", "the update's id (the status file and audit entries carry it)").option("--to <version>", "the version to install").option("--back", "go back to the previous install (app.old)").action(async (o) => {
82
82
  if (!o.back === !o.to) throw new Error("Give either --to or --back.");
83
- const { runUpdateJob } = await import("./update-BSRYrxCc.js");
83
+ const { runUpdateJob } = await import("./update-D3csyVVU.js");
84
84
  if ((await runUpdateJob({
85
85
  dataDir: o.data,
86
86
  id: o.id,
@@ -1 +1 @@
1
- {"version":3,"file":"config-DaBg-G1g.js","names":[],"sources":["../server/config.ts"],"sourcesContent":["import { chmodSync, existsSync, mkdirSync, readdirSync, statSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join, resolve } from \"node:path\";\nimport { envVar, LEGACY_PRODUCT, pickDir, PRODUCT, underService } from \"./brand.ts\";\n\nexport interface Config {\n dataDir: string;\n dbPath: string;\n masterKeyPath: string;\n host: string;\n port: number;\n /** Dev mode: console served by Vite on :5173, server only serves /api, /mcp, /oauth. */\n dev: boolean;\n /** Honor X-Forwarded-For / X-Forwarded-Proto (behind a reverse proxy). */\n trustProxy: boolean;\n /** Running under `xaq service install` (XAQ_SERVICE=1 in the unit/plist; AGENTINBOX_SERVICE=1 in units from before the rename). */\n service: boolean;\n version: string;\n /**\n * Base URLs for Google APIs. Overridable so tests can point at test/fake-google.ts.\n * Defaults: https://oauth2.googleapis.com, https://accounts.google.com, https://gmail.googleapis.com, https://pubsub.googleapis.com,\n * https://www.googleapis.com (Calendar: paths start with /calendar/v3), https://www.googleapis.com (Drive: /drive/v3).\n */\n google: {\n oauthBase: string;\n authBase: string;\n gmailBase: string;\n pubsubBase: string;\n calendarBase: string;\n /**\n * Drive, Docs, Sheets and Slides (core's Drive client, server/google/drive.ts, and a code extension's API probe,\n * server/google/apis.ts): one base for all of them (tests: the fake). Unset: Drive uses https://www.googleapis.com\n * and the probe each API's own https://<service>.\n */\n apisBase?: string;\n };\n}\n\nexport interface ConfigInput {\n dataDir?: string;\n host?: string;\n port?: number;\n dev?: boolean;\n}\n\nexport const DEFAULT_PORT = 8425;\n\n/** New installs keep their data here. */\nexport const newDataDir = () => join(homedir(), `.${PRODUCT}`);\n/** Where installs from before the rename keep theirs; still used unless `~/.xaq` holds our files. */\nexport const legacyDataDir = () => join(homedir(), `.${LEGACY_PRODUCT}`);\n/** Files only our daemon creates in its data directory: a `~/.xaq` holding one is ours. */\nconst DATA_MARKERS = [`${PRODUCT}.db`, `${LEGACY_PRODUCT}.db`, \"master.key\", `${PRODUCT}.lock`, `${LEGACY_PRODUCT}.lock`];\nconst pickDataDir = () => pickDir(homedir(), DATA_MARKERS, { dot: true });\n\n/**\n * The data directory when neither --data nor XAQ_DATA / AGENTINBOX_DATA says: `~/.xaq` when it holds our\n * files; else an existing `~/.agentinbox` (from before the rename); else `~/.xaq`. A `~/.xaq` without our\n * files belongs to another program called xaq: then our files only go in beside its own (see restrictDir).\n * Nothing is moved: master.key stays put.\n */\nexport const defaultDataDir = () => resolve(pickDataDir().dir);\n\nlet toldLegacy = false;\n\n/**\n * The data directory: --data, else XAQ_DATA (AGENTINBOX_DATA), else the default. Says once per process\n * when the default is the pre-rename `~/.agentinbox` (unless `quiet`).\n */\nexport function resolveDataDir(flag?: string, opts: { quiet?: boolean } = {}): string {\n const given = flag ?? (envVar(\"DATA\") || undefined);\n if (given !== undefined) return resolve(given);\n const { dir, kind } = pickDataDir();\n if (kind === \"legacy\" && !opts.quiet && !toldLegacy) {\n toldLegacy = true;\n process.stderr.write(`${PRODUCT}: using the existing data directory ${dir} (from before agentinbox was renamed to ${PRODUCT}; new installs use ${newDataDir()}).\\n`);\n }\n return resolve(dir);\n}\n\n/**\n * The database, lock and log files in a data directory: `xaq.*`, or in a data directory from before the\n * rename (it has agentinbox.db and no xaq.db) `agentinbox.*`. The lock keeps the name an older\n * agentinbox (from before the rename) still running on that directory uses, so the two never run at once.\n */\nexport function dataFiles(dataDir: string): { db: string; lock: string; log: string } {\n const legacy = !existsSync(join(dataDir, `${PRODUCT}.db`)) && existsSync(join(dataDir, `${LEGACY_PRODUCT}.db`));\n const base = legacy ? LEGACY_PRODUCT : PRODUCT;\n return { db: join(dataDir, `${base}.db`), lock: join(dataDir, `${base}.lock`), log: join(dataDir, `${base}.log`) };\n}\n\nexport function loadConfig(input: ConfigInput = {}, version = \"0.0.0\"): Config {\n const dataDir = resolveDataDir(input.dataDir);\n // Only a directory that already had files in it can have exposed anything.\n const existed = hasFiles(dataDir);\n mkdirSync(dataDir, { recursive: true, mode: 0o700 });\n // A ~/.xaq that also holds another program's files (it is called xaq too) isn't ours to chmod.\n if (!hasForeignFiles(dataDir)) restrictDir(dataDir, existed);\n return {\n dataDir,\n dbPath: dataFiles(dataDir).db,\n masterKeyPath: join(dataDir, \"master.key\"),\n host: input.host ?? process.env.HOST ?? \"0.0.0.0\",\n port: input.port ?? (process.env.PORT ? Number(process.env.PORT) : DEFAULT_PORT),\n dev: input.dev ?? false,\n trustProxy: process.env.TRUST_PROXY === \"1\",\n service: underService(),\n version,\n google: {\n oauthBase: envVar(\"GOOGLE_OAUTH_BASE\") ?? \"https://oauth2.googleapis.com\",\n authBase: envVar(\"GOOGLE_AUTH_BASE\") ?? \"https://accounts.google.com\",\n gmailBase: envVar(\"GOOGLE_GMAIL_BASE\") ?? \"https://gmail.googleapis.com\",\n pubsubBase: envVar(\"GOOGLE_PUBSUB_BASE\") ?? \"https://pubsub.googleapis.com\",\n calendarBase: envVar(\"GOOGLE_CALENDAR_BASE\") ?? \"https://www.googleapis.com\",\n ...(envVar(\"GOOGLE_APIS_BASE\") ? { apisBase: envVar(\"GOOGLE_APIS_BASE\")! } : {}),\n },\n };\n}\n\n/** What our daemon keeps in its data directory (names only; nothing is read). */\n// (install.lock: the listeners' shared install directory holds app* and that lock; same rule there.)\n// (extensions, extension-state: installed extensions and code extensions' state, docs/EXTENSIONS.md.)\n// (attachments: files get_attachment saved for agents, server/mail/saved-attachments.ts.)\n// (update: the console update job's status, log and database snapshot, server/update.ts.)\nconst OWN_ENTRY =\n /^(?:(?:xaq|agentinbox)\\.(?:db(?:-wal|-shm|-journal)?|lock|log)|master\\.key|setup-token|service\\.env|app(?:\\.new|\\.old)?|install\\.lock|units|backups?|update|extensions|extension-state|attachments)$/;\n\n/** Whether the directory holds anything our daemon didn't put there. */\nexport const hasForeignFiles = (dir: string) => {\n try {\n return readdirSync(dir).some((n) => !OWN_ENTRY.test(n));\n } catch {\n return false;\n }\n};\n\nconst hasFiles = (dir: string) => {\n try {\n return readdirSync(dir).length > 0;\n } catch {\n return false;\n }\n};\n\n/**\n * An existing data dir may be group/world accessible (umask, manual mkdir): tighten it to owner-only.\n * Only a directory that already held files gets a notice; a new (or empty) one is tightened silently.\n */\nfunction restrictDir(dir: string, existed: boolean): void {\n try {\n if (process.platform !== \"win32\" && statSync(dir).mode & 0o077) {\n chmodSync(dir, 0o700);\n if (existed) process.stderr.write(`xaq: made ${dir} private to your user (it was readable by other users).\\n`);\n }\n } catch {\n // Not ours to change; the files inside are still created owner-only.\n }\n}\n"],"mappings":";;;;;;AA6CA,MAAa,eAAe;;AAG5B,MAAa,mBAAmB,KAAK,QAAQ,GAAG,MAAa;;AAE7D,MAAa,sBAAsB,KAAK,QAAQ,GAAG,IAAI,gBAAgB;;AAEvE,MAAM,eAAe;CAAC;CAAiB,GAAG,eAAe;CAAM;CAAc;CAAmB,GAAG,eAAe;AAAM;AACxH,MAAM,oBAAoB,QAAQ,QAAQ,GAAG,cAAc,EAAE,KAAK,KAAK,CAAC;;;;;;;AAQxE,MAAa,uBAAuB,QAAQ,YAAY,CAAC,CAAC,GAAG;AAE7D,IAAI,aAAa;;;;;AAMjB,SAAgB,eAAe,MAAe,OAA4B,CAAC,GAAW;CACpF,MAAM,QAAQ,SAAS,OAAO,MAAM,KAAK,KAAA;CACzC,IAAI,UAAU,KAAA,GAAW,OAAO,QAAQ,KAAK;CAC7C,MAAM,EAAE,KAAK,SAAS,YAAY;CAClC,IAAI,SAAS,YAAY,CAAC,KAAK,SAAS,CAAC,YAAY;EACnD,aAAa;EACb,QAAQ,OAAO,MAAM,0CAAiD,IAAI,gEAAuE,WAAW,EAAE,KAAK;CACrK;CACA,OAAO,QAAQ,GAAG;AACpB;;;;;;AAOA,SAAgB,UAAU,SAA4D;CAEpF,MAAM,OADS,CAAC,WAAW,KAAK,SAAS,QAAe,CAAC,KAAK,WAAW,KAAK,SAAS,eAAsB,CAAC,IACxF,iBAAA;CACtB,OAAO;EAAE,IAAI,KAAK,SAAS,GAAG,KAAK,IAAI;EAAG,MAAM,KAAK,SAAS,GAAG,KAAK,MAAM;EAAG,KAAK,KAAK,SAAS,GAAG,KAAK,KAAK;CAAE;AACnH;AAEA,SAAgB,WAAW,QAAqB,CAAC,GAAG,UAAU,SAAiB;CAC7E,MAAM,UAAU,eAAe,MAAM,OAAO;CAE5C,MAAM,UAAU,SAAS,OAAO;CAChC,UAAU,SAAS;EAAE,WAAW;EAAM,MAAM;CAAM,CAAC;CAEnD,IAAI,CAAC,gBAAgB,OAAO,GAAG,YAAY,SAAS,OAAO;CAC3D,OAAO;EACL;EACA,QAAQ,UAAU,OAAO,CAAC,CAAC;EAC3B,eAAe,KAAK,SAAS,YAAY;EACzC,MAAM,MAAM,QAAQ,QAAQ,IAAI,QAAQ;EACxC,MAAM,MAAM,SAAS,QAAQ,IAAI,OAAO,OAAO,QAAQ,IAAI,IAAI,IAAA;EAC/D,KAAK,MAAM,OAAO;EAClB,YAAY,QAAQ,IAAI,gBAAgB;EACxC,SAAS,aAAa;EACtB;EACA,QAAQ;GACN,WAAW,OAAO,mBAAmB,KAAK;GAC1C,UAAU,OAAO,kBAAkB,KAAK;GACxC,WAAW,OAAO,mBAAmB,KAAK;GAC1C,YAAY,OAAO,oBAAoB,KAAK;GAC5C,cAAc,OAAO,sBAAsB,KAAK;GAChD,GAAI,OAAO,kBAAkB,IAAI,EAAE,UAAU,OAAO,kBAAkB,EAAG,IAAI,CAAC;EAChF;CACF;AACF;;AAOA,MAAM,YACJ;;AAGF,MAAa,mBAAmB,QAAgB;CAC9C,IAAI;EACF,OAAO,YAAY,GAAG,CAAC,CAAC,MAAM,MAAM,CAAC,UAAU,KAAK,CAAC,CAAC;CACxD,QAAQ;EACN,OAAO;CACT;AACF;AAEA,MAAM,YAAY,QAAgB;CAChC,IAAI;EACF,OAAO,YAAY,GAAG,CAAC,CAAC,SAAS;CACnC,QAAQ;EACN,OAAO;CACT;AACF;;;;;AAMA,SAAS,YAAY,KAAa,SAAwB;CACxD,IAAI;EACF,IAAI,QAAQ,aAAa,WAAW,SAAS,GAAG,CAAC,CAAC,OAAO,IAAO;GAC9D,UAAU,KAAK,GAAK;GACpB,IAAI,SAAS,QAAQ,OAAO,MAAM,aAAa,IAAI,0DAA0D;EAC/G;CACF,QAAQ,CAER;AACF"}
1
+ {"version":3,"file":"config-DaBg-G1g.js","names":[],"sources":["../server/config.ts"],"sourcesContent":["import { chmodSync, existsSync, mkdirSync, readdirSync, statSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join, resolve } from \"node:path\";\nimport { envVar, LEGACY_PRODUCT, pickDir, PRODUCT, underService } from \"./brand.ts\";\n\nexport interface Config {\n dataDir: string;\n dbPath: string;\n masterKeyPath: string;\n host: string;\n port: number;\n /** Dev mode: console served by Vite on :5173, server only serves /api, /mcp, /oauth. */\n dev: boolean;\n /** Honor X-Forwarded-For / X-Forwarded-Proto (behind a reverse proxy). */\n trustProxy: boolean;\n /** Running under `xaq service install` (XAQ_SERVICE=1 in the unit/plist; AGENTINBOX_SERVICE=1 in units from before the rename). */\n service: boolean;\n version: string;\n /**\n * Base URLs for Google APIs. Overridable so tests can point at test/fake-google.ts.\n * Defaults: https://oauth2.googleapis.com, https://accounts.google.com, https://gmail.googleapis.com, https://pubsub.googleapis.com,\n * https://www.googleapis.com (Calendar: paths start with /calendar/v3), https://www.googleapis.com (Drive: /drive/v3).\n */\n google: {\n oauthBase: string;\n authBase: string;\n gmailBase: string;\n pubsubBase: string;\n calendarBase: string;\n /**\n * Drive, Docs, Sheets and Slides (core's Drive client, server/google/drive.ts, and a code extension's API probe,\n * server/google/apis.ts): one base for all of them (tests: the fake). Unset: Drive uses https://www.googleapis.com\n * and the probe each API's own https://<service>.\n */\n apisBase?: string;\n };\n}\n\nexport interface ConfigInput {\n dataDir?: string;\n host?: string;\n port?: number;\n dev?: boolean;\n}\n\nexport const DEFAULT_PORT = 8425;\n\n/** New installs keep their data here. */\nexport const newDataDir = () => join(homedir(), `.${PRODUCT}`);\n/** Where installs from before the rename keep theirs; still used unless `~/.xaq` holds our files. */\nexport const legacyDataDir = () => join(homedir(), `.${LEGACY_PRODUCT}`);\n/** Files only our daemon creates in its data directory: a `~/.xaq` holding one is ours. */\nconst DATA_MARKERS = [`${PRODUCT}.db`, `${LEGACY_PRODUCT}.db`, \"master.key\", `${PRODUCT}.lock`, `${LEGACY_PRODUCT}.lock`];\nconst pickDataDir = () => pickDir(homedir(), DATA_MARKERS, { dot: true });\n\n/**\n * The data directory when neither --data nor XAQ_DATA / AGENTINBOX_DATA says: `~/.xaq` when it holds our\n * files; else an existing `~/.agentinbox` (from before the rename); else `~/.xaq`. A `~/.xaq` without our\n * files belongs to another program called xaq: then our files only go in beside its own (see restrictDir).\n * Nothing is moved: master.key stays put.\n */\nexport const defaultDataDir = () => resolve(pickDataDir().dir);\n\nlet toldLegacy = false;\n\n/**\n * The data directory: --data, else XAQ_DATA (AGENTINBOX_DATA), else the default. Says once per process\n * when the default is the pre-rename `~/.agentinbox` (unless `quiet`).\n */\nexport function resolveDataDir(flag?: string, opts: { quiet?: boolean } = {}): string {\n const given = flag ?? (envVar(\"DATA\") || undefined);\n if (given !== undefined) return resolve(given);\n const { dir, kind } = pickDataDir();\n if (kind === \"legacy\" && !opts.quiet && !toldLegacy) {\n toldLegacy = true;\n process.stderr.write(`${PRODUCT}: using the existing data directory ${dir} (from before agentinbox was renamed to ${PRODUCT}; new installs use ${newDataDir()}).\\n`);\n }\n return resolve(dir);\n}\n\n/**\n * The database, lock and log files in a data directory: `xaq.*`, or in a data directory from before the\n * rename (it has agentinbox.db and no xaq.db) `agentinbox.*`. The lock keeps the name an older\n * agentinbox (from before the rename) still running on that directory uses, so the two never run at once.\n */\nexport function dataFiles(dataDir: string): { db: string; lock: string; log: string } {\n const legacy = !existsSync(join(dataDir, `${PRODUCT}.db`)) && existsSync(join(dataDir, `${LEGACY_PRODUCT}.db`));\n const base = legacy ? LEGACY_PRODUCT : PRODUCT;\n return { db: join(dataDir, `${base}.db`), lock: join(dataDir, `${base}.lock`), log: join(dataDir, `${base}.log`) };\n}\n\nexport function loadConfig(input: ConfigInput = {}, version = \"0.0.0\"): Config {\n const dataDir = resolveDataDir(input.dataDir);\n // Only a directory that already had files in it can have exposed anything.\n const existed = hasFiles(dataDir);\n mkdirSync(dataDir, { recursive: true, mode: 0o700 });\n // A ~/.xaq that also holds another program's files (it is called xaq too) isn't ours to chmod.\n if (!hasForeignFiles(dataDir)) restrictDir(dataDir, existed);\n return {\n dataDir,\n dbPath: dataFiles(dataDir).db,\n masterKeyPath: join(dataDir, \"master.key\"),\n host: input.host ?? process.env.HOST ?? \"0.0.0.0\",\n port: input.port ?? (process.env.PORT ? Number(process.env.PORT) : DEFAULT_PORT),\n dev: input.dev ?? false,\n trustProxy: process.env.TRUST_PROXY === \"1\",\n service: underService(),\n version,\n google: {\n oauthBase: envVar(\"GOOGLE_OAUTH_BASE\") ?? \"https://oauth2.googleapis.com\",\n authBase: envVar(\"GOOGLE_AUTH_BASE\") ?? \"https://accounts.google.com\",\n gmailBase: envVar(\"GOOGLE_GMAIL_BASE\") ?? \"https://gmail.googleapis.com\",\n pubsubBase: envVar(\"GOOGLE_PUBSUB_BASE\") ?? \"https://pubsub.googleapis.com\",\n calendarBase: envVar(\"GOOGLE_CALENDAR_BASE\") ?? \"https://www.googleapis.com\",\n ...(envVar(\"GOOGLE_APIS_BASE\") ? { apisBase: envVar(\"GOOGLE_APIS_BASE\")! } : {}),\n },\n };\n}\n\n/** What our daemon keeps in its data directory (names only; nothing is read). */\n// (install.lock: the listeners' shared install directory holds app* and that lock; same rule there.)\n// (extensions, extension-state: installed extensions and code extensions' state, docs/EXTENSIONS.md.)\n// (attachments: files get_attachment saved for agents, server/mail/saved-attachments.ts.)\n// (update: the console update job's status, lock, log and database snapshots, server/update.ts.)\nconst OWN_ENTRY =\n /^(?:(?:xaq|agentinbox)\\.(?:db(?:-wal|-shm|-journal)?|lock|log)|master\\.key|setup-token|service\\.env|app(?:\\.new|\\.old)?|install\\.lock|units|backups?|update|extensions|extension-state|attachments)$/;\n\n/** Whether the directory holds anything our daemon didn't put there. */\nexport const hasForeignFiles = (dir: string) => {\n try {\n return readdirSync(dir).some((n) => !OWN_ENTRY.test(n));\n } catch {\n return false;\n }\n};\n\nconst hasFiles = (dir: string) => {\n try {\n return readdirSync(dir).length > 0;\n } catch {\n return false;\n }\n};\n\n/**\n * An existing data dir may be group/world accessible (umask, manual mkdir): tighten it to owner-only.\n * Only a directory that already held files gets a notice; a new (or empty) one is tightened silently.\n */\nfunction restrictDir(dir: string, existed: boolean): void {\n try {\n if (process.platform !== \"win32\" && statSync(dir).mode & 0o077) {\n chmodSync(dir, 0o700);\n if (existed) process.stderr.write(`xaq: made ${dir} private to your user (it was readable by other users).\\n`);\n }\n } catch {\n // Not ours to change; the files inside are still created owner-only.\n }\n}\n"],"mappings":";;;;;;AA6CA,MAAa,eAAe;;AAG5B,MAAa,mBAAmB,KAAK,QAAQ,GAAG,MAAa;;AAE7D,MAAa,sBAAsB,KAAK,QAAQ,GAAG,IAAI,gBAAgB;;AAEvE,MAAM,eAAe;CAAC;CAAiB,GAAG,eAAe;CAAM;CAAc;CAAmB,GAAG,eAAe;AAAM;AACxH,MAAM,oBAAoB,QAAQ,QAAQ,GAAG,cAAc,EAAE,KAAK,KAAK,CAAC;;;;;;;AAQxE,MAAa,uBAAuB,QAAQ,YAAY,CAAC,CAAC,GAAG;AAE7D,IAAI,aAAa;;;;;AAMjB,SAAgB,eAAe,MAAe,OAA4B,CAAC,GAAW;CACpF,MAAM,QAAQ,SAAS,OAAO,MAAM,KAAK,KAAA;CACzC,IAAI,UAAU,KAAA,GAAW,OAAO,QAAQ,KAAK;CAC7C,MAAM,EAAE,KAAK,SAAS,YAAY;CAClC,IAAI,SAAS,YAAY,CAAC,KAAK,SAAS,CAAC,YAAY;EACnD,aAAa;EACb,QAAQ,OAAO,MAAM,0CAAiD,IAAI,gEAAuE,WAAW,EAAE,KAAK;CACrK;CACA,OAAO,QAAQ,GAAG;AACpB;;;;;;AAOA,SAAgB,UAAU,SAA4D;CAEpF,MAAM,OADS,CAAC,WAAW,KAAK,SAAS,QAAe,CAAC,KAAK,WAAW,KAAK,SAAS,eAAsB,CAAC,IACxF,iBAAA;CACtB,OAAO;EAAE,IAAI,KAAK,SAAS,GAAG,KAAK,IAAI;EAAG,MAAM,KAAK,SAAS,GAAG,KAAK,MAAM;EAAG,KAAK,KAAK,SAAS,GAAG,KAAK,KAAK;CAAE;AACnH;AAEA,SAAgB,WAAW,QAAqB,CAAC,GAAG,UAAU,SAAiB;CAC7E,MAAM,UAAU,eAAe,MAAM,OAAO;CAE5C,MAAM,UAAU,SAAS,OAAO;CAChC,UAAU,SAAS;EAAE,WAAW;EAAM,MAAM;CAAM,CAAC;CAEnD,IAAI,CAAC,gBAAgB,OAAO,GAAG,YAAY,SAAS,OAAO;CAC3D,OAAO;EACL;EACA,QAAQ,UAAU,OAAO,CAAC,CAAC;EAC3B,eAAe,KAAK,SAAS,YAAY;EACzC,MAAM,MAAM,QAAQ,QAAQ,IAAI,QAAQ;EACxC,MAAM,MAAM,SAAS,QAAQ,IAAI,OAAO,OAAO,QAAQ,IAAI,IAAI,IAAA;EAC/D,KAAK,MAAM,OAAO;EAClB,YAAY,QAAQ,IAAI,gBAAgB;EACxC,SAAS,aAAa;EACtB;EACA,QAAQ;GACN,WAAW,OAAO,mBAAmB,KAAK;GAC1C,UAAU,OAAO,kBAAkB,KAAK;GACxC,WAAW,OAAO,mBAAmB,KAAK;GAC1C,YAAY,OAAO,oBAAoB,KAAK;GAC5C,cAAc,OAAO,sBAAsB,KAAK;GAChD,GAAI,OAAO,kBAAkB,IAAI,EAAE,UAAU,OAAO,kBAAkB,EAAG,IAAI,CAAC;EAChF;CACF;AACF;;AAOA,MAAM,YACJ;;AAGF,MAAa,mBAAmB,QAAgB;CAC9C,IAAI;EACF,OAAO,YAAY,GAAG,CAAC,CAAC,MAAM,MAAM,CAAC,UAAU,KAAK,CAAC,CAAC;CACxD,QAAQ;EACN,OAAO;CACT;AACF;AAEA,MAAM,YAAY,QAAgB;CAChC,IAAI;EACF,OAAO,YAAY,GAAG,CAAC,CAAC,SAAS;CACnC,QAAQ;EACN,OAAO;CACT;AACF;;;;;AAMA,SAAS,YAAY,KAAa,SAAwB;CACxD,IAAI;EACF,IAAI,QAAQ,aAAa,WAAW,SAAS,GAAG,CAAC,CAAC,OAAO,IAAO;GAC9D,UAAU,KAAK,GAAK;GACpB,IAAI,SAAS,QAAQ,OAAO,MAAM,aAAa,IAAI,0DAA0D;EAC/G;CACF,QAAQ,CAER;AACF"}
@@ -22,7 +22,7 @@ import { a as childArgs, c as ensureStateDir, d as removeConnectionState, l as e
22
22
  import { K as systemOps, nt as recommendedNodeWarning } from "./service-DoSR8dSk.js";
23
23
  import { a as networkAddresses, i as loopbackUrl, n as isPrivateIPv4, r as isWildcardHost, t as consoleUrls } from "./urls-U_LfYV61.js";
24
24
  import { $ as accountScopes, A as importClient, B as buildUrl, C as postToken, Ct as threadLabels, D as clientCredentials, E as callbackMode, Et as router, F as setClient, G as sleep, H as hasReason, I as Approvals, J as AccountNeedsReauthError, K as CALENDAR_PUSH_PATH, L as CANCELLED_PREFIX, M as projectId$1, N as recordPreviousRedirect, O as clientView, P as redirectWarning, Q as accountCalendar, R as REQUEST_TIMEOUT_MS, S as googleRuntime, St as reserveSend, T as refreshExpiryKind, Tt as readBody, U as isAbortError, V as errorFromResponse, W as isOutcomeUnknown, X as GoogleApiError, Y as DownscopeError, Z as ScopesNotGrantedError, _ as contentPool, _t as missingScopes, a as smokeSubjectPrefix, at as CALENDAR_SCOPES, b as createGmailClient, bt as disallowedRecipients, c as attachmentOutput, ct as EXTRA_SCOPE_SETS, d as closeAttachmentParser, dt as IDENTITY_TOKEN_SCOPES, et as allRows, ft as MAIL_SCOPES, g as closeContentPool, gt as isExtensionScope, h as ToolError, ht as hasScopes, it as updateRow, j as markRedirectVerified, k as clientWarnings, l as formatAttachmentList, lt as GMAIL_SCOPE, m as MAX_ATTACHMENT_BYTES, mt as describeScopes, nt as getRow, o as attachmentArgs, ot as CORE_ONLY_SCOPES, p as AgentMail, q as createCalendarPush, r as isSmokeDir, rt as toAccount, st as EXTENSION_SCOPE_RE, t as SMOKE_NAME_RE, tt as createAccountsService, u as pruneSavedAttachments, ut as GOOGLE_SERVICE_RE, v as DRIVE_EXPORT_FORMATS, vt as scopeGrants, w as refreshExpiry, wt as ADMIN_ACTOR, x as googleAccessToken, xt as inScope, y as mimeTools, yt as scopeWithout, z as asHttpError } from "./smoke-DIWhlw6m.js";
25
- import { a as newUpdateId, d as selfUpdateSupport, i as launchUpdateJob, n as activeUpdate, o as previousVersion, s as readUpdateStatus, v as writeUpdateStatus } from "./update-DSvn3KhM.js";
25
+ import { D as compareVersions, E as writeUpdateStatus, a as launchUpdateJob, c as previousVersion, d as releaseUpdateLock, h as selfUpdateSupport, r as claimUpdateLock, s as newUpdateId, u as readUpdateStatus } from "./update-D4RFE-jl.js";
26
26
  import { s as renderSkill } from "./skill-Dgc-4Dop.js";
27
27
  import { t as SettingsStore } from "./settings-BRKLADPA.js";
28
28
  import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, rmdirSync, statSync } from "node:fs";
@@ -16940,7 +16940,13 @@ function systemApi(ctx, host = selfUpdateHost(ctx)) {
16940
16940
  audit("denied", "lockout");
16941
16941
  return lockedOut(c, banned);
16942
16942
  }
16943
- const body = await readBody(c, SelfUpdateBody);
16943
+ let body;
16944
+ try {
16945
+ body = await readBody(c, SelfUpdateBody);
16946
+ } catch (err) {
16947
+ audit("denied", "invalid_request", { error: errorMessage(err) });
16948
+ throw err;
16949
+ }
16944
16950
  const hash = ctx.settings.getState("admin.passwordHash");
16945
16951
  const check = await checkSecret(ctx, ip, body.password, async () => !!hash && await verifyPasswordAsync(body.password, hash));
16946
16952
  if (!check.ok) {
@@ -16958,59 +16964,74 @@ function systemApi(ctx, host = selfUpdateHost(ctx)) {
16958
16964
  };
16959
16965
  const support = host.support();
16960
16966
  if (!support.supported) refuse("unsupported", support.reason ?? "This instance can't update itself.");
16961
- const running = activeUpdate(dataDir);
16962
- if (running) refuse("in_progress", `An update to ${running.to} is already running.`, { running: running.id });
16963
- let to;
16964
- if (body.action === "update") {
16965
- const latest = await host.latest();
16966
- if (!latest) refuse("no_latest", "Couldn't reach npm to check the latest version. Try again in a few minutes.");
16967
- if (body.version !== latest || compareVersions(latest, current) <= 0) refuse("not_latest", `${body.version ?? "That version"} isn't the latest release newer than ${current}${latest ? ` (${latest} is)` : ""}. Reload and try again.`, { version: body.version ?? null });
16968
- to = latest;
16969
- } else {
16970
- const previous = previousVersion(dataDir, current);
16971
- if (!previous) refuse("no_previous", "There is no previous install to go back to.");
16972
- to = previous;
16967
+ const id = newUpdateId();
16968
+ const lock = claimUpdateLock(dataDir, id);
16969
+ if (!lock.ok) {
16970
+ const running = readUpdateStatus(dataDir);
16971
+ refuse("in_progress", `${running?.id === lock.holder.id && running.state === "running" ? running.kind === "update" ? `An update to ${running.to}` : `Going back to ${running.to}` : "An update"} is already running. Wait for it to finish.`, { running: lock.holder.id });
16973
16972
  }
16974
- const status = {
16975
- id: newUpdateId(),
16976
- kind: body.action,
16977
- from: current,
16978
- to,
16979
- state: "running",
16980
- step: "starting",
16981
- message: null,
16982
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
16983
- finishedAt: null
16984
- };
16985
- const detail = {
16986
- action: body.action,
16987
- id: status.id,
16988
- from: current,
16989
- to
16990
- };
16991
- writeUpdateStatus(dataDir, status);
16973
+ let status;
16992
16974
  try {
16993
- await host.launch({
16994
- dataDir,
16995
- id: status.id,
16996
- kind: status.kind,
16997
- version: to
16998
- });
16975
+ let to;
16976
+ if (body.action === "update") {
16977
+ const latest = await host.latest();
16978
+ if (!latest) refuse("no_latest", "Couldn't reach npm to check the latest version. Try again in a few minutes.");
16979
+ if (body.version !== latest || compareVersions(latest, current) <= 0) refuse("not_latest", `${body.version ?? "That version"} isn't the latest release newer than ${current}${latest ? ` (${latest} is)` : ""}. Reload and try again.`, { version: body.version ?? null });
16980
+ to = latest;
16981
+ } else {
16982
+ const previous = previousVersion(dataDir, current);
16983
+ if (!previous) refuse("no_previous", "There is no older install to go back to.");
16984
+ to = previous;
16985
+ }
16986
+ status = {
16987
+ id,
16988
+ kind: body.action,
16989
+ from: current,
16990
+ to,
16991
+ state: "running",
16992
+ step: "starting",
16993
+ message: null,
16994
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
16995
+ finishedAt: null
16996
+ };
16997
+ const detail = {
16998
+ action: body.action,
16999
+ id,
17000
+ from: current,
17001
+ to
17002
+ };
17003
+ writeUpdateStatus(dataDir, status);
17004
+ try {
17005
+ await host.launch({
17006
+ dataDir,
17007
+ id,
17008
+ kind: status.kind,
17009
+ version: to
17010
+ });
17011
+ } catch (err) {
17012
+ const message = `Couldn't start the update job: ${errorMessage(err)}`;
17013
+ writeUpdateStatus(dataDir, {
17014
+ ...status,
17015
+ state: "failed",
17016
+ message,
17017
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString()
17018
+ });
17019
+ audit("error", "launch_failed", {
17020
+ ...detail,
17021
+ message
17022
+ });
17023
+ throw new HttpError(500, "update_failed", message);
17024
+ }
16999
17025
  } catch (err) {
17000
- const message = `Couldn't start the update job: ${errorMessage(err)}`;
17001
- writeUpdateStatus(dataDir, {
17002
- ...status,
17003
- state: "failed",
17004
- message,
17005
- finishedAt: (/* @__PURE__ */ new Date()).toISOString()
17006
- });
17007
- audit("error", "launch_failed", {
17008
- ...detail,
17009
- message
17010
- });
17011
- throw new HttpError(500, "update_failed", message);
17026
+ releaseUpdateLock(dataDir, id);
17027
+ throw err;
17012
17028
  }
17013
- audit("ok", null, detail);
17029
+ audit("ok", null, {
17030
+ action: body.action,
17031
+ id,
17032
+ from: current,
17033
+ to: status.to
17034
+ });
17014
17035
  return c.json(status, 202);
17015
17036
  });
17016
17037
  }
@@ -17032,41 +17053,6 @@ async function latestVersion(ctx) {
17032
17053
  };
17033
17054
  return version;
17034
17055
  }
17035
- /**
17036
- * Semver precedence: numeric core, then a pre-release sorts before its release, and pre-release
17037
- * identifiers compare numerically when both are numbers ("nightly.10" > "nightly.9").
17038
- */
17039
- function compareVersions(a, b) {
17040
- const parse = (v) => {
17041
- const clean = v.trim().replace(/^v/, "").split("+")[0];
17042
- const dash = clean.indexOf("-");
17043
- return {
17044
- parts: (dash === -1 ? clean : clean.slice(0, dash)).split(".").map((n) => Number.parseInt(n, 10) || 0),
17045
- pre: dash === -1 ? null : clean.slice(dash + 1).split(".")
17046
- };
17047
- };
17048
- const x = parse(a);
17049
- const y = parse(b);
17050
- for (let i = 0; i < Math.max(x.parts.length, y.parts.length); i++) {
17051
- const d = (x.parts[i] ?? 0) - (y.parts[i] ?? 0);
17052
- if (d) return Math.sign(d);
17053
- }
17054
- if (!x.pre || !y.pre) return x.pre === y.pre ? 0 : x.pre ? -1 : 1;
17055
- for (let i = 0; i < Math.max(x.pre.length, y.pre.length); i++) {
17056
- const p = x.pre[i];
17057
- const q = y.pre[i];
17058
- if (p === void 0) return -1;
17059
- if (q === void 0) return 1;
17060
- const pn = /^\d+$/.test(p);
17061
- const qn = /^\d+$/.test(q);
17062
- if (pn && qn) {
17063
- const d = Number(p) - Number(q);
17064
- if (d) return Math.sign(d);
17065
- } else if (pn !== qn) return pn ? -1 : 1;
17066
- else if (p !== q) return p < q ? -1 : 1;
17067
- }
17068
- return 0;
17069
- }
17070
17056
  /** Runs, or reinstalls as a service, the latest version with this instance's options. */
17071
17057
  function upgradeCommand(config, env = process.env) {
17072
17058
  const proxyEnv = config.trustProxy ? ["TRUST_PROXY=1", ...["TRUST_PROXY_HOPS", "TRUST_PROXY_FROM"].filter((k) => env[k]?.trim()).map((k) => `${k}=${shellQuote$1(env[k].trim())}`)] : [];
@@ -20526,4 +20512,4 @@ function openBrowser(url) {
20526
20512
  //#endregion
20527
20513
  export { runForeground };
20528
20514
 
20529
- //# sourceMappingURL=server-TeB2QMu0.js.map
20515
+ //# sourceMappingURL=server-C0QWLPW3.js.map