@wspc/cli 0.1.3 → 0.1.4

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
@@ -1464,9 +1464,9 @@ function createConsistencyFetch(opts) {
1464
1464
  }
1465
1465
 
1466
1466
  // src/version.ts
1467
- var VERSION = "0.1.3";
1467
+ var VERSION = "0.1.4";
1468
1468
  var SPEC_SHA = "e0bbc1e3";
1469
- var SPEC_FETCHED_AT = "2026-06-22T01:23:52.693Z";
1469
+ var SPEC_FETCHED_AT = "2026-06-22T06:55:37.490Z";
1470
1470
  var API_BASE = "https://api.wspc.ai";
1471
1471
 
1472
1472
  // src/index.ts
@@ -3294,7 +3294,7 @@ var pushTestCommand = new Command45("test").description("Send a test push notifi
3294
3294
  }
3295
3295
  render({ kind: "push_test", display: { "shape": "object", "fields": ["ok", "status", "detail", "durationMs"], "format": { "ok": "bool-badge" } } }, result.data);
3296
3296
  if (result.data?.ok === false) {
3297
- process.exit(1);
3297
+ process.exitCode = 1;
3298
3298
  }
3299
3299
  });
3300
3300
 
@@ -3872,13 +3872,15 @@ async function ensureClientId(opts) {
3872
3872
  apiBase: opts.baseUrl,
3873
3873
  fetchImpl: opts.fetchImpl
3874
3874
  });
3875
- const c = await opts.store.read();
3876
- const existing = c.envs[opts.envName]?.client_id;
3875
+ let existing;
3876
+ await opts.store.update((c) => {
3877
+ existing = c.envs[opts.envName]?.client_id;
3878
+ if (existing) return;
3879
+ const targetEnv = c.envs[opts.envName] ??= { api_base: opts.baseUrl, accounts: {} };
3880
+ targetEnv.api_base = opts.baseUrl;
3881
+ targetEnv.accounts ??= {};
3882
+ });
3877
3883
  if (existing) return existing;
3878
- const targetEnv = c.envs[opts.envName] ??= { api_base: opts.baseUrl, accounts: {} };
3879
- targetEnv.api_base = opts.baseUrl;
3880
- targetEnv.accounts ??= {};
3881
- await opts.store.write(c);
3882
3884
  const res = await fetchImpl(`${opts.baseUrl}/auth/oauth/register`, {
3883
3885
  method: "POST",
3884
3886
  headers: { "content-type": "application/json" },
@@ -3894,12 +3896,18 @@ async function ensureClientId(opts) {
3894
3896
  }
3895
3897
  const body = await res.json();
3896
3898
  if (!body.client_id) throw new Error("client_registration_failed: missing client_id in response");
3897
- const fresh = await opts.store.read();
3898
- const env = fresh.envs[opts.envName] ?? { api_base: opts.baseUrl, accounts: {} };
3899
- env.client_id = body.client_id;
3900
- fresh.envs[opts.envName] = env;
3901
- await opts.store.write(fresh);
3902
- return body.client_id;
3899
+ let clientId = body.client_id;
3900
+ await opts.store.update((c) => {
3901
+ const env = c.envs[opts.envName] ??= { api_base: opts.baseUrl, accounts: {} };
3902
+ env.api_base = opts.baseUrl;
3903
+ env.accounts ??= {};
3904
+ if (env.client_id) {
3905
+ clientId = env.client_id;
3906
+ return;
3907
+ }
3908
+ env.client_id = body.client_id;
3909
+ });
3910
+ return clientId;
3903
3911
  }
3904
3912
 
3905
3913
  // src/handwritten/auth/fetch-me.ts
@@ -3956,36 +3964,36 @@ async function runLogin(opts) {
3956
3964
  const now = opts.now ?? Date.now;
3957
3965
  const me = opts.fetchMe ?? ((o) => fetchMe(o));
3958
3966
  if (opts.apiKey) {
3959
- const initial = await opts.store.read();
3960
- getOrCreateEnv(initial, envName, opts.baseUrl);
3961
- initial.current_env = envName;
3962
- await opts.store.write(initial);
3967
+ await opts.store.update((initial) => {
3968
+ getOrCreateEnv(initial, envName, opts.baseUrl);
3969
+ initial.current_env = envName;
3970
+ });
3963
3971
  const who2 = await me({
3964
3972
  baseUrl: opts.baseUrl,
3965
3973
  token: opts.apiKey,
3966
3974
  store: opts.store,
3967
3975
  envName
3968
3976
  });
3969
- const c2 = await opts.store.read();
3970
- const env2 = getOrCreateEnv(c2, envName, opts.baseUrl);
3971
- const prev2 = env2.accounts[who2.email] ?? { email: who2.email };
3972
- const acct2 = env2.accounts[who2.email] = {
3973
- ...prev2,
3974
- email: who2.email,
3975
- user_id: who2.user_id,
3976
- api_key: opts.apiKey
3977
- };
3978
- delete acct2.refresh_token;
3979
- delete acct2.access_token;
3980
- delete acct2.access_token_expires_at;
3981
- env2.current_account = who2.email;
3982
- if (who2.email !== LEGACY_ACCOUNT_KEY) delete env2.accounts[LEGACY_ACCOUNT_KEY];
3983
- c2.current_env = envName;
3984
- await opts.store.write(c2);
3977
+ await opts.store.update((c) => {
3978
+ const env = getOrCreateEnv(c, envName, opts.baseUrl);
3979
+ const prev = env.accounts[who2.email] ?? { email: who2.email };
3980
+ const acct = env.accounts[who2.email] = {
3981
+ ...prev,
3982
+ email: who2.email,
3983
+ user_id: who2.user_id,
3984
+ api_key: opts.apiKey
3985
+ };
3986
+ delete acct.refresh_token;
3987
+ delete acct.access_token;
3988
+ delete acct.access_token_expires_at;
3989
+ env.current_account = who2.email;
3990
+ if (who2.email !== LEGACY_ACCOUNT_KEY) delete env.accounts[LEGACY_ACCOUNT_KEY];
3991
+ c.current_env = envName;
3992
+ });
3985
3993
  opts.output.write(`${green("\u2713")} logged in (api key) as ${bold(who2.email)} ${dim(`\u2192 env "${envName}"`)}`);
3986
3994
  return;
3987
3995
  }
3988
- const ensureClient = opts.ensureClient ?? ((env2) => ensureClientId({ store: opts.store, envName: env2, baseUrl: opts.baseUrl }));
3996
+ const ensureClient = opts.ensureClient ?? ((env) => ensureClientId({ store: opts.store, envName: env, baseUrl: opts.baseUrl }));
3989
3997
  const clientId = opts.clientId ?? await ensureClient(envName);
3990
3998
  const flow = opts.deviceFlow ?? runDeviceFlow;
3991
3999
  const result = await flow({
@@ -4005,22 +4013,22 @@ async function runLogin(opts) {
4005
4013
  store: opts.store,
4006
4014
  envName
4007
4015
  });
4008
- const c = await opts.store.read();
4009
- const env = getOrCreateEnv(c, envName, opts.baseUrl);
4010
- const prev = env.accounts[who.email] ?? { email: who.email };
4011
- const acct = env.accounts[who.email] = {
4012
- ...prev,
4013
- email: who.email,
4014
- user_id: who.user_id,
4015
- refresh_token: result.refresh_token,
4016
- access_token: result.access_token,
4017
- access_token_expires_at: now() + result.expires_in * 1e3
4018
- };
4019
- delete acct.api_key;
4020
- env.current_account = who.email;
4021
- if (who.email !== LEGACY_ACCOUNT_KEY) delete env.accounts[LEGACY_ACCOUNT_KEY];
4022
- c.current_env = envName;
4023
- await opts.store.write(c);
4016
+ await opts.store.update((c) => {
4017
+ const env = getOrCreateEnv(c, envName, opts.baseUrl);
4018
+ const prev = env.accounts[who.email] ?? { email: who.email };
4019
+ const acct = env.accounts[who.email] = {
4020
+ ...prev,
4021
+ email: who.email,
4022
+ user_id: who.user_id,
4023
+ refresh_token: result.refresh_token,
4024
+ access_token: result.access_token,
4025
+ access_token_expires_at: now() + result.expires_in * 1e3
4026
+ };
4027
+ delete acct.api_key;
4028
+ env.current_account = who.email;
4029
+ if (who.email !== LEGACY_ACCOUNT_KEY) delete env.accounts[LEGACY_ACCOUNT_KEY];
4030
+ c.current_env = envName;
4031
+ });
4024
4032
  opts.output.writeJson({ event: "login_success", email: who.email });
4025
4033
  opts.output.write(`${green("\u2713")} logged in as ${bold(who.email)} ${dim(`\u2192 env "${envName}"`)}`);
4026
4034
  }
@@ -4057,27 +4065,32 @@ import { Command as Command63 } from "commander";
4057
4065
 
4058
4066
  // src/handwritten/auth/logout.ts
4059
4067
  async function runLogout(opts) {
4060
- const c = await opts.store.read();
4061
- const envName = opts.envName ?? c.current_env;
4062
- if (!envName || !c.envs[envName]) return { removed: [] };
4063
- const env = c.envs[envName];
4064
- env.accounts ??= {};
4065
- if (opts.all) {
4066
- const removed = Object.keys(env.accounts);
4067
- env.accounts = {};
4068
- env.current_account = void 0;
4069
- await opts.store.write(c);
4070
- return { removed };
4071
- }
4072
- const target = opts.email ?? env.current_account ?? (Object.keys(env.accounts).length === 1 ? Object.keys(env.accounts)[0] : void 0);
4073
- if (!target || !env.accounts[target]) return { removed: [] };
4074
- delete env.accounts[target];
4075
- if (env.current_account === target) {
4076
- const remaining = Object.keys(env.accounts);
4077
- env.current_account = remaining.length === 1 ? remaining[0] : void 0;
4078
- }
4079
- await opts.store.write(c);
4080
- return { removed: [target], newActive: env.current_account };
4068
+ let removed = [];
4069
+ let newActive;
4070
+ let removedSingleAccount = false;
4071
+ await opts.store.update((c) => {
4072
+ const envName = opts.envName ?? c.current_env;
4073
+ if (!envName || !c.envs[envName]) return;
4074
+ const env = c.envs[envName];
4075
+ env.accounts ??= {};
4076
+ if (opts.all) {
4077
+ removed = Object.keys(env.accounts);
4078
+ env.accounts = {};
4079
+ env.current_account = void 0;
4080
+ return;
4081
+ }
4082
+ const target = opts.email ?? env.current_account ?? (Object.keys(env.accounts).length === 1 ? Object.keys(env.accounts)[0] : void 0);
4083
+ if (!target || !env.accounts[target]) return;
4084
+ delete env.accounts[target];
4085
+ removed = [target];
4086
+ removedSingleAccount = true;
4087
+ if (env.current_account === target) {
4088
+ const remaining = Object.keys(env.accounts);
4089
+ env.current_account = remaining.length === 1 ? remaining[0] : void 0;
4090
+ }
4091
+ newActive = env.current_account;
4092
+ });
4093
+ return removedSingleAccount ? { removed, newActive } : { removed };
4081
4094
  }
4082
4095
 
4083
4096
  // src/handwritten/commands/logout.ts
@@ -4121,10 +4134,9 @@ registerRenderer("whoami", (data) => {
4121
4134
  }
4122
4135
  });
4123
4136
  async function backfillActiveEmail(store, envName, email, userId) {
4124
- const cfg = await store.read();
4125
- if (rekeyLegacyAccount(cfg, envName, email, userId)) {
4126
- await store.write(cfg);
4127
- }
4137
+ await store.update((cfg) => {
4138
+ rekeyLegacyAccount(cfg, envName, email, userId);
4139
+ });
4128
4140
  }
4129
4141
  var whoamiCommand = new Command64("whoami").description("Show the active env, signed-in account, and organization").action(async () => {
4130
4142
  const store = new ConfigStore();
@@ -4199,24 +4211,24 @@ registerRenderer("config_show", (data) => {
4199
4211
  process.stdout.write(table(headers, rows));
4200
4212
  });
4201
4213
  async function setConfigKey(store, key, value) {
4202
- const c = await store.read();
4203
- const resolved = resolveAccount(c, { accountOverride: process.env.WSPC_ACCOUNT });
4204
- const env = c.envs[resolved.envName];
4205
- if (!env) throw new Error(`env "${resolved.envName}" not found`);
4206
- const acct = env.accounts[resolved.email];
4207
- if (!acct) throw new Error(`account "${resolved.email}" not found`);
4208
- switch (key) {
4209
- case "actor":
4210
- if (value !== "user" && value !== "agent") throw new Error("actor must be 'user' or 'agent'");
4211
- acct.actor = value;
4212
- break;
4213
- case "agent-label":
4214
- acct.agent_label = value;
4215
- break;
4216
- default:
4217
- throw new Error(`unknown config key: ${key}`);
4218
- }
4219
- await store.write(c);
4214
+ await store.update((c) => {
4215
+ const resolved = resolveAccount(c, { accountOverride: process.env.WSPC_ACCOUNT });
4216
+ const env = c.envs[resolved.envName];
4217
+ if (!env) throw new Error(`env "${resolved.envName}" not found`);
4218
+ const acct = env.accounts[resolved.email];
4219
+ if (!acct) throw new Error(`account "${resolved.email}" not found`);
4220
+ switch (key) {
4221
+ case "actor":
4222
+ if (value !== "user" && value !== "agent") throw new Error("actor must be 'user' or 'agent'");
4223
+ acct.actor = value;
4224
+ break;
4225
+ case "agent-label":
4226
+ acct.agent_label = value;
4227
+ break;
4228
+ default:
4229
+ throw new Error(`unknown config key: ${key}`);
4230
+ }
4231
+ });
4220
4232
  }
4221
4233
  configCommand.command("show").description("List configured envs (tokens redacted, current marked with \u2713)").action(async () => {
4222
4234
  const c = await new ConfigStore().read();
@@ -4242,10 +4254,10 @@ configCommand.command("set <key> <value>").description("Set a field on the activ
4242
4254
  });
4243
4255
  configCommand.command("use <env>").description("Switch current_env").action(async (env) => {
4244
4256
  const store = new ConfigStore();
4245
- const c = await store.read();
4246
- if (!c.envs[env]) throw new Error(`env "${env}" not found`);
4247
- c.current_env = env;
4248
- await store.write(c);
4257
+ await store.update((c) => {
4258
+ if (!c.envs[env]) throw new Error(`env "${env}" not found`);
4259
+ c.current_env = env;
4260
+ });
4249
4261
  process.stdout.write(`\u2713 current_env=${env}
4250
4262
  `);
4251
4263
  });
@@ -4266,15 +4278,15 @@ async function listAccounts(store) {
4266
4278
  }));
4267
4279
  }
4268
4280
  async function switchAccount(store, email) {
4269
- const c = await store.read();
4270
- const envName = c.current_env;
4271
- if (!envName || !c.envs[envName]) throw new Error("no current env; run `wspc login` first");
4272
- const env = c.envs[envName];
4273
- if (!env.accounts?.[email]) {
4274
- throw new Error(`no account '${email}' in env '${envName}'. Run \`wspc account ls\` or \`wspc login\`.`);
4275
- }
4276
- env.current_account = email;
4277
- await store.write(c);
4281
+ await store.update((c) => {
4282
+ const envName = c.current_env;
4283
+ if (!envName || !c.envs[envName]) throw new Error("no current env; run `wspc login` first");
4284
+ const env = c.envs[envName];
4285
+ if (!env.accounts?.[email]) {
4286
+ throw new Error(`no account '${email}' in env '${envName}'. Run \`wspc account ls\` or \`wspc login\`.`);
4287
+ }
4288
+ env.current_account = email;
4289
+ });
4278
4290
  }
4279
4291
  registerRenderer("account_ls", (data) => {
4280
4292
  const rows = data.accounts;
@@ -4495,11 +4507,12 @@ import { pipeline } from "stream/promises";
4495
4507
  function parseContentDispositionFilename(header) {
4496
4508
  if (!header) return void 0;
4497
4509
  if (!header.toLowerCase().startsWith("attachment")) return void 0;
4498
- const quoted = header.match(/filename="([^"]+)"/i);
4499
- if (quoted) return quoted[1];
4510
+ const quoted = header.match(/filename="([^"]*)"/i);
4500
4511
  const unquoted = header.match(/filename=([^;\s]+)/i);
4501
- if (unquoted) return unquoted[1];
4502
- return void 0;
4512
+ const filename = quoted?.[1] ?? unquoted?.[1];
4513
+ if (!filename || filename === "." || filename === "..") return void 0;
4514
+ if (filename.includes("/") || filename.includes("\\")) return void 0;
4515
+ return filename;
4503
4516
  }
4504
4517
 
4505
4518
  // src/handwritten/commands/email/attachment.ts
@@ -6064,7 +6077,11 @@ function createDriveRealtimeSource(args) {
6064
6077
  const message = parseDriveRealtimeMessage(data);
6065
6078
  if (message.type === "ready") {
6066
6079
  if (message.replayed > 0) {
6067
- handlers?.onEvent(optionalString({ debounce_ms: 2e3, reason: "ready_replay" }, "cursor", message.cursor));
6080
+ handlers?.onEvent({
6081
+ debounce_ms: 2e3,
6082
+ reason: "ready_replay",
6083
+ ...message.cursor === void 0 ? {} : { cursor: message.cursor }
6084
+ });
6068
6085
  }
6069
6086
  if (message.cursor !== void 0) {
6070
6087
  await persistBestEffort({ ...currentRealtime, last_cursor: message.cursor });
@@ -6072,10 +6089,12 @@ function createDriveRealtimeSource(args) {
6072
6089
  return;
6073
6090
  }
6074
6091
  if (message.type === "library_changed") {
6075
- handlers?.onEvent(optionalString(optionalString({
6092
+ handlers?.onEvent({
6076
6093
  debounce_ms: 2e3,
6077
- reason: "library_changed"
6078
- }, "cursor", message.cursor), "path", message.path));
6094
+ reason: "library_changed",
6095
+ ...message.cursor === void 0 ? {} : { cursor: message.cursor },
6096
+ ...message.path === void 0 ? {} : { path: message.path }
6097
+ });
6079
6098
  if (message.cursor !== void 0) {
6080
6099
  await persistBestEffort({ ...currentRealtime, last_cursor: message.cursor, last_event_at: driveIsoTimestamp(clock) });
6081
6100
  }
@@ -6083,7 +6102,11 @@ function createDriveRealtimeSource(args) {
6083
6102
  }
6084
6103
  if (message.type === "resync_required") {
6085
6104
  const reason = message.reason ?? "resync_required";
6086
- handlers?.onEvent(optionalString({ immediate: true, reason }, "cursor", message.cursor));
6105
+ handlers?.onEvent({
6106
+ immediate: true,
6107
+ reason,
6108
+ ...message.cursor === void 0 ? {} : { cursor: message.cursor }
6109
+ });
6087
6110
  if (message.cursor !== void 0 || isInvalidCursorReason(reason)) {
6088
6111
  await persistBestEffort(resyncRealtimeState(currentRealtime, message.cursor, reason, driveIsoTimestamp(clock)));
6089
6112
  }
@@ -6148,22 +6171,37 @@ function parseDriveRealtimeMessage(raw) {
6148
6171
  const messageType = typeof value.type === "string" ? value.type : void 0;
6149
6172
  const cursor = typeof value.cursor === "string" ? value.cursor : void 0;
6150
6173
  if (messageType === "ready") {
6151
- return optionalString({ type: "ready", replayed: typeof value.replayed === "number" ? value.replayed : 0 }, "cursor", cursor);
6174
+ return {
6175
+ type: "ready",
6176
+ replayed: typeof value.replayed === "number" ? value.replayed : 0,
6177
+ ...cursor === void 0 ? {} : { cursor }
6178
+ };
6152
6179
  }
6153
6180
  if (messageType === "library_changed") {
6154
- return optionalString(optionalString({ type: "library_changed" }, "cursor", cursor), "path", value.path);
6181
+ return {
6182
+ type: "library_changed",
6183
+ ...cursor === void 0 ? {} : { cursor },
6184
+ ...typeof value.path === "string" ? { path: value.path } : {}
6185
+ };
6155
6186
  }
6156
6187
  if (messageType === "resync_required") {
6157
- return optionalString(optionalString({ type: "resync_required" }, "cursor", cursor), "reason", value.reason);
6188
+ return {
6189
+ type: "resync_required",
6190
+ ...cursor === void 0 ? {} : { cursor },
6191
+ ...typeof value.reason === "string" ? { reason: value.reason } : {}
6192
+ };
6158
6193
  }
6159
6194
  if (messageType === "error") {
6160
- return optionalString(
6161
- optionalString({ type: "error" }, "code", value.code),
6162
- "message",
6163
- typeof value.message === "string" ? redactedRealtimeError(value.message) : void 0
6164
- );
6195
+ return {
6196
+ type: "error",
6197
+ ...typeof value.code === "string" ? { code: value.code } : {},
6198
+ ...typeof value.message === "string" ? { message: redactedRealtimeError(value.message) } : {}
6199
+ };
6165
6200
  }
6166
- return optionalString({ type: "unknown" }, "message_type", messageType);
6201
+ return {
6202
+ type: "unknown",
6203
+ ...messageType === void 0 ? {} : { message_type: messageType }
6204
+ };
6167
6205
  }
6168
6206
  function redactedRealtimeError(error) {
6169
6207
  const text = error instanceof Error ? error.message : String(error);
@@ -6187,7 +6225,11 @@ function resyncRealtimeState(realtime, cursor, reason, lastEventAt) {
6187
6225
  const { last_cursor: _lastCursor, ...next } = realtime;
6188
6226
  return { ...next, last_event_at: lastEventAt };
6189
6227
  }
6190
- return optionalString({ ...realtime, last_event_at: lastEventAt }, "last_cursor", cursor);
6228
+ return {
6229
+ ...realtime,
6230
+ last_event_at: lastEventAt,
6231
+ ...cursor === void 0 ? {} : { last_cursor: cursor }
6232
+ };
6191
6233
  }
6192
6234
  function isInvalidCursorReason(reason) {
6193
6235
  return /\bcursor[_ -]?(invalid|expired|gone|missing|not[_ -]?found)\b|\binvalid[_ -]?cursor\b/i.test(reason);
@@ -6195,12 +6237,6 @@ function isInvalidCursorReason(reason) {
6195
6237
  function isRealtimeAuthError(error) {
6196
6238
  return /\b(401|403|auth|authorization|unauthorized|forbidden)\b/i.test(String(error));
6197
6239
  }
6198
- function optionalString(target, key, value) {
6199
- if (typeof value !== "string") {
6200
- return target;
6201
- }
6202
- return { ...target, [key]: value };
6203
- }
6204
6240
  function nativeWebSocketConnector(url, handlers, init) {
6205
6241
  const WebSocketWithInit = WebSocket;
6206
6242
  const ws = new WebSocketWithInit(url.toString(), init);