@wspc/cli 0.1.4 → 0.1.6
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 +111 -58
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/spec/openapi.json +21 -3
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command74 } from "commander";
|
|
5
5
|
import { realpathSync } from "fs";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
7
|
|
|
@@ -1178,6 +1178,11 @@ var todoCommentUpdate = (options) => (options.client ?? client).patch({
|
|
|
1178
1178
|
...options.headers
|
|
1179
1179
|
}
|
|
1180
1180
|
});
|
|
1181
|
+
var projectDelete = (options) => (options.client ?? client).delete({
|
|
1182
|
+
security: [{ scheme: "bearer", type: "http" }],
|
|
1183
|
+
url: "/todo/projects/{id}",
|
|
1184
|
+
...options
|
|
1185
|
+
});
|
|
1181
1186
|
var recurrenceRuleDelete = (options) => (options.client ?? client).delete({
|
|
1182
1187
|
security: [{ scheme: "bearer", type: "http" }],
|
|
1183
1188
|
url: "/todo/recurrence-rules/{id}",
|
|
@@ -1464,9 +1469,9 @@ function createConsistencyFetch(opts) {
|
|
|
1464
1469
|
}
|
|
1465
1470
|
|
|
1466
1471
|
// src/version.ts
|
|
1467
|
-
var VERSION = "0.1.
|
|
1468
|
-
var SPEC_SHA = "
|
|
1469
|
-
var SPEC_FETCHED_AT = "2026-06-
|
|
1472
|
+
var VERSION = "0.1.6";
|
|
1473
|
+
var SPEC_SHA = "706a2577";
|
|
1474
|
+
var SPEC_FETCHED_AT = "2026-06-24T08:28:50.392Z";
|
|
1470
1475
|
var API_BASE = "https://api.wspc.ai";
|
|
1471
1476
|
|
|
1472
1477
|
// src/index.ts
|
|
@@ -3586,9 +3591,30 @@ var todoCommentUpdateCommand = new Command56("edit").description("Edit a comment
|
|
|
3586
3591
|
render({ kind: "todo_comment_update", display: { "shape": "object", "format": { "id": "id-short", "todo_id": "id-short", "user_id": "id-short", "created_at": "relative-time", "updated_at": "relative-time", "deleted_at": "relative-time" } } }, result.data);
|
|
3587
3592
|
});
|
|
3588
3593
|
|
|
3589
|
-
// src/generated/cli/todo/
|
|
3594
|
+
// src/generated/cli/todo/project/rm.ts
|
|
3590
3595
|
import { Command as Command57 } from "commander";
|
|
3591
|
-
var
|
|
3596
|
+
var projectDeleteCommand = new Command57("rm").description("Soft-delete a project").argument("<id>", "id").action(async (id, opts) => {
|
|
3597
|
+
const client2 = await loadSdkClient();
|
|
3598
|
+
const result = await projectDelete({
|
|
3599
|
+
client: client2._rawClient,
|
|
3600
|
+
path: {
|
|
3601
|
+
id
|
|
3602
|
+
}
|
|
3603
|
+
});
|
|
3604
|
+
if (result.error || !result.response?.ok) {
|
|
3605
|
+
process.stderr.write(
|
|
3606
|
+
`HTTP ${result.response?.status ?? "?"}: ${JSON.stringify(result.error ?? "unknown error", null, 2)}
|
|
3607
|
+
`
|
|
3608
|
+
);
|
|
3609
|
+
process.exitCode = 1;
|
|
3610
|
+
return;
|
|
3611
|
+
}
|
|
3612
|
+
render({ kind: "project_delete", display: { "shape": "object", "format": { "id": "id-short", "org_id": "id-short", "creator_user_id": "id-short", "default_todo_type_id": "id-short", "name": "truncate", "created_at": "relative-time", "updated_at": "relative-time", "deleted_at": "relative-time" } } }, result.data);
|
|
3613
|
+
});
|
|
3614
|
+
|
|
3615
|
+
// src/generated/cli/todo/rule/rm.ts
|
|
3616
|
+
import { Command as Command58 } from "commander";
|
|
3617
|
+
var recurrenceRuleDeleteCommand = new Command58("rm").description("Delete a recurring todo rule").argument("<id>", "id").option("--expected-version <value>", "expected_version").action(async (id, opts) => {
|
|
3592
3618
|
const client2 = await loadSdkClient();
|
|
3593
3619
|
const result = await recurrenceRuleDelete({
|
|
3594
3620
|
client: client2._rawClient,
|
|
@@ -3611,8 +3637,8 @@ var recurrenceRuleDeleteCommand = new Command57("rm").description("Delete a recu
|
|
|
3611
3637
|
});
|
|
3612
3638
|
|
|
3613
3639
|
// src/generated/cli/todo/rule/show.ts
|
|
3614
|
-
import { Command as
|
|
3615
|
-
var recurrenceRuleGetCommand = new
|
|
3640
|
+
import { Command as Command59 } from "commander";
|
|
3641
|
+
var recurrenceRuleGetCommand = new Command59("show").description("Get a recurring todo rule").argument("<id>", "id").action(async (id, opts) => {
|
|
3616
3642
|
const client2 = await loadSdkClient();
|
|
3617
3643
|
const result = await recurrenceRuleGet({
|
|
3618
3644
|
client: client2._rawClient,
|
|
@@ -3632,8 +3658,8 @@ var recurrenceRuleGetCommand = new Command58("show").description("Get a recurrin
|
|
|
3632
3658
|
});
|
|
3633
3659
|
|
|
3634
3660
|
// src/generated/cli/todo/rm.ts
|
|
3635
|
-
import { Command as
|
|
3636
|
-
var todoDeleteCommand = new
|
|
3661
|
+
import { Command as Command60 } from "commander";
|
|
3662
|
+
var todoDeleteCommand = new Command60("rm").description("Soft-delete a todo").argument("<id>", "id").option("--expected-version <value>", "expected_version").option("--cascade <value>", "cascade").action(async (id, opts) => {
|
|
3637
3663
|
const client2 = await loadSdkClient();
|
|
3638
3664
|
const result = await todoDelete({
|
|
3639
3665
|
client: client2._rawClient,
|
|
@@ -3657,8 +3683,8 @@ var todoDeleteCommand = new Command59("rm").description("Soft-delete a todo").ar
|
|
|
3657
3683
|
});
|
|
3658
3684
|
|
|
3659
3685
|
// src/generated/cli/todo/show.ts
|
|
3660
|
-
import { Command as
|
|
3661
|
-
var todoGetCommand = new
|
|
3686
|
+
import { Command as Command61 } from "commander";
|
|
3687
|
+
var todoGetCommand = new Command61("show").description("Get a todo by id").argument("<id>", "id").option("--include-deleted <value>", "include_deleted").option("--include-orphan-fields <value>", "include_orphan_fields").action(async (id, opts) => {
|
|
3662
3688
|
const client2 = await loadSdkClient();
|
|
3663
3689
|
const result = await todoGet({
|
|
3664
3690
|
client: client2._rawClient,
|
|
@@ -3683,8 +3709,8 @@ var todoGetCommand = new Command60("show").description("Get a todo by id").argum
|
|
|
3683
3709
|
});
|
|
3684
3710
|
|
|
3685
3711
|
// src/generated/cli/todo/update.ts
|
|
3686
|
-
import { Command as
|
|
3687
|
-
var todoUpdateCommand = new
|
|
3712
|
+
import { Command as Command62 } from "commander";
|
|
3713
|
+
var todoUpdateCommand = new Command62("update").description("Update a todo").argument("<id>", "id").option("--expected-version <value>", "expected_version").option("--title <value>", "New title. Omit to leave the existing title unchanged. Must be non-empty when supplied.").option("--description <value>", 'New description. Markdown formatted (CommonMark + GFM tables, strikethrough, task lists). Pass empty string `""` explicitly to clear an existing description, or omit to leave unchanged. Passing `null` is strictly rejected.').option("--parent-id <value>", "Re-parent the todo. Pass a valid parent ID to attach under another todo, pass `null` to move it back to the root level, or omit to leave unchanged. Nesting is limited to one level; attempting to set a child todo as a parent will trigger `PARENT_IS_CHILD`.").option("--status <value>", "New status of the todo. Allowed transitions: `open` \u2794 `in_progress` \u2794 `done`. `cancelled` represents a terminal state. Transitioning to `done` automatically emits a `captureTodoCompleted` analytics event. Omit to leave the existing status unchanged.").option("--due-at <value>", 'Update calendar due date in ISO date-only format (`YYYY-MM-DD`). Pass `""` explicitly to clear an existing due date, or omit to leave it unchanged. Passing `null` is strictly rejected.').option("--type-id <value>", "Re-assign this todo to a different active type. The new type must belong to the todo's same project; otherwise the request fails with TYPE_PROJECT_MISMATCH. New server-generated type ids use typ_<ULID>; legacy ids remain accepted.").option("--custom-fields <value>", "PATCH semantics: only the keys present in this map change. Pass `null` for a key (e.g. `custom_fields: { priority: null }`) to explicitly delete that custom field value. Array values are replaced wholesale with no element-level diff. Providing a key that is not declared on the effective todo type is rejected with `UNDECLARED_FIELD`.").option("--user-id <value>", "Reassign the owner (assignee) user ID of this todo. Target user must belong to the same organization.").action(async (id, opts) => {
|
|
3688
3714
|
const client2 = await loadSdkClient();
|
|
3689
3715
|
const result = await todoUpdate({
|
|
3690
3716
|
client: client2._rawClient,
|
|
@@ -3785,6 +3811,7 @@ function registerGeneratedCommands(root) {
|
|
|
3785
3811
|
const root_todo_project = root_todo.command("project").description("project commands");
|
|
3786
3812
|
root_todo_project.addCommand(projectCreateCommand);
|
|
3787
3813
|
root_todo_project.addCommand(projectListCommand);
|
|
3814
|
+
root_todo_project.addCommand(projectDeleteCommand);
|
|
3788
3815
|
const root_todo_rule = root_todo.command("rule").description("rule commands");
|
|
3789
3816
|
root_todo_rule.addCommand(recurrenceRuleCreateCommand);
|
|
3790
3817
|
root_todo_rule.addCommand(recurrenceRuleListCommand);
|
|
@@ -3800,7 +3827,7 @@ function registerGeneratedCommands(root) {
|
|
|
3800
3827
|
}
|
|
3801
3828
|
|
|
3802
3829
|
// src/handwritten/commands/login.ts
|
|
3803
|
-
import { Command as
|
|
3830
|
+
import { Command as Command63 } from "commander";
|
|
3804
3831
|
|
|
3805
3832
|
// src/handwritten/auth/device-flow.ts
|
|
3806
3833
|
var DEFAULT_SLEEP = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -4042,7 +4069,7 @@ function resolveLoginTarget(opts, env) {
|
|
|
4042
4069
|
function wantsJson(opts, env) {
|
|
4043
4070
|
return opts.json === true || env.WSPC_OUTPUT === "json";
|
|
4044
4071
|
}
|
|
4045
|
-
var loginCommand = new
|
|
4072
|
+
var loginCommand = new Command63("login").description("Log in via OAuth device flow (default) or API key").option("--api-key <key>", "Log in with a wspc API key (escape hatch)").option("--api-base <url>", "Target API base URL (default: production)").option("--env <name>", "Config env name to store credentials under").option("--json", "Emit machine-readable events to stdout").action(async (opts) => {
|
|
4046
4073
|
const store = new ConfigStore();
|
|
4047
4074
|
const { baseUrl, envName } = resolveLoginTarget(opts, process.env);
|
|
4048
4075
|
const output = wantsJson(opts, process.env) ? { write: () => {
|
|
@@ -4061,7 +4088,7 @@ var loginCommand = new Command62("login").description("Log in via OAuth device f
|
|
|
4061
4088
|
});
|
|
4062
4089
|
|
|
4063
4090
|
// src/handwritten/commands/logout.ts
|
|
4064
|
-
import { Command as
|
|
4091
|
+
import { Command as Command64 } from "commander";
|
|
4065
4092
|
|
|
4066
4093
|
// src/handwritten/auth/logout.ts
|
|
4067
4094
|
async function runLogout(opts) {
|
|
@@ -4094,7 +4121,7 @@ async function runLogout(opts) {
|
|
|
4094
4121
|
}
|
|
4095
4122
|
|
|
4096
4123
|
// src/handwritten/commands/logout.ts
|
|
4097
|
-
var logoutCommand = new
|
|
4124
|
+
var logoutCommand = new Command64("logout").description("Log out an account (default: the active account in the current env)").argument("[email]", "Email of the account to log out").option("--all", "Log out every account in the current env").action(async (email, opts) => {
|
|
4098
4125
|
const res = await runLogout({ store: new ConfigStore(), email, all: opts.all });
|
|
4099
4126
|
if (res.removed.length === 0) {
|
|
4100
4127
|
process.stdout.write("nothing to log out\n");
|
|
@@ -4107,7 +4134,7 @@ var logoutCommand = new Command63("logout").description("Log out an account (def
|
|
|
4107
4134
|
});
|
|
4108
4135
|
|
|
4109
4136
|
// src/handwritten/commands/whoami.ts
|
|
4110
|
-
import { Command as
|
|
4137
|
+
import { Command as Command65 } from "commander";
|
|
4111
4138
|
var ENV_DISPLAY = {
|
|
4112
4139
|
shape: "object",
|
|
4113
4140
|
fields: ["name", "api_base", "account", "actor", "agent_label"]
|
|
@@ -4138,7 +4165,7 @@ async function backfillActiveEmail(store, envName, email, userId) {
|
|
|
4138
4165
|
rekeyLegacyAccount(cfg, envName, email, userId);
|
|
4139
4166
|
});
|
|
4140
4167
|
}
|
|
4141
|
-
var whoamiCommand = new
|
|
4168
|
+
var whoamiCommand = new Command65("whoami").description("Show the active env, signed-in account, and organization").action(async () => {
|
|
4142
4169
|
const store = new ConfigStore();
|
|
4143
4170
|
const config = await store.read();
|
|
4144
4171
|
let resolved;
|
|
@@ -4191,8 +4218,8 @@ function printLoggedOut() {
|
|
|
4191
4218
|
}
|
|
4192
4219
|
|
|
4193
4220
|
// src/handwritten/commands/config.ts
|
|
4194
|
-
import { Command as
|
|
4195
|
-
var configCommand = new
|
|
4221
|
+
import { Command as Command66 } from "commander";
|
|
4222
|
+
var configCommand = new Command66("config").description("Manage wspc local config");
|
|
4196
4223
|
registerRenderer("config_show", (data) => {
|
|
4197
4224
|
const d = data;
|
|
4198
4225
|
if (d.envs.length === 0) {
|
|
@@ -4263,7 +4290,7 @@ configCommand.command("use <env>").description("Switch current_env").action(asyn
|
|
|
4263
4290
|
});
|
|
4264
4291
|
|
|
4265
4292
|
// src/handwritten/commands/account.ts
|
|
4266
|
-
import { Command as
|
|
4293
|
+
import { Command as Command67 } from "commander";
|
|
4267
4294
|
async function listAccounts(store) {
|
|
4268
4295
|
const c = await store.read();
|
|
4269
4296
|
const envName = c.current_env;
|
|
@@ -4304,7 +4331,7 @@ registerRenderer("account_ls", (data) => {
|
|
|
4304
4331
|
]);
|
|
4305
4332
|
process.stdout.write(table(headers, body));
|
|
4306
4333
|
});
|
|
4307
|
-
var accountCommand = new
|
|
4334
|
+
var accountCommand = new Command67("account").description("Manage logged-in accounts");
|
|
4308
4335
|
accountCommand.command("ls").description("List accounts in the current env (active marked with \u2713)").action(async () => {
|
|
4309
4336
|
const accounts = await listAccounts(new ConfigStore());
|
|
4310
4337
|
render({ kind: "account_ls" }, { accounts });
|
|
@@ -4316,7 +4343,7 @@ accountCommand.command("switch <email>").description("Set the active account for
|
|
|
4316
4343
|
});
|
|
4317
4344
|
|
|
4318
4345
|
// src/handwritten/commands/todo-done.ts
|
|
4319
|
-
import { Command as
|
|
4346
|
+
import { Command as Command68 } from "commander";
|
|
4320
4347
|
var TODO_UPDATE_DISPLAY = {
|
|
4321
4348
|
shape: "object",
|
|
4322
4349
|
format: {
|
|
@@ -4334,7 +4361,7 @@ var TODO_UPDATE_DISPLAY = {
|
|
|
4334
4361
|
deleted_at: "relative-time"
|
|
4335
4362
|
}
|
|
4336
4363
|
};
|
|
4337
|
-
var todoDoneCommand = new
|
|
4364
|
+
var todoDoneCommand = new Command68("done").description("Mark a todo done (sugar for `update <id> --status done`)").argument("<id>", "Todo id").action(async (id) => {
|
|
4338
4365
|
const client2 = await loadSdkClient();
|
|
4339
4366
|
const result = await todoUpdate({
|
|
4340
4367
|
client: client2._rawClient,
|
|
@@ -4353,7 +4380,8 @@ var todoDoneCommand = new Command67("done").description("Mark a todo done (sugar
|
|
|
4353
4380
|
});
|
|
4354
4381
|
|
|
4355
4382
|
// src/handwritten/commands/email/send.ts
|
|
4356
|
-
import { Command as
|
|
4383
|
+
import { Command as Command69 } from "commander";
|
|
4384
|
+
import { randomUUID } from "crypto";
|
|
4357
4385
|
import { readFile, stat } from "fs/promises";
|
|
4358
4386
|
import { basename } from "path";
|
|
4359
4387
|
|
|
@@ -4411,7 +4439,7 @@ async function resolveAttachment(input) {
|
|
|
4411
4439
|
`--attach ${input}: neither a readable file nor a valid <prefix>_<ulid>:<idx> reference.`
|
|
4412
4440
|
);
|
|
4413
4441
|
}
|
|
4414
|
-
var sendCommand = new
|
|
4442
|
+
var sendCommand = new Command69("send").description("Send an outbound email").requiredOption("--from <alias-email>", "alias email to send from").option("--to <addr...>", "recipient address (repeatable)", []).option("--subject <text>", "subject").option("--text <body>", "plain-text body").option("--text-file <path>", "read text body from file").option("--reply <id>", "inbound email id to reply to").option("--attach <path-or-ref...>", "attachment (file path or eml_xxx:idx)", []).option("--idempotency-key <key>", "idempotency key (auto-generated if omitted)").action(async (opts) => {
|
|
4415
4443
|
const isReply = Boolean(opts.reply);
|
|
4416
4444
|
const to = opts.to;
|
|
4417
4445
|
const attachInputs = opts.attach;
|
|
@@ -4469,7 +4497,9 @@ var sendCommand = new Command68("send").description("Send an outbound email").re
|
|
|
4469
4497
|
const body = {
|
|
4470
4498
|
from_alias_email: opts.from,
|
|
4471
4499
|
text,
|
|
4472
|
-
idempotency_key
|
|
4500
|
+
// Server requires an idempotency_key; a fresh random one makes each send a
|
|
4501
|
+
// distinct request, which is the right default for one-shot CLI sends.
|
|
4502
|
+
idempotency_key: opts.idempotencyKey ?? randomUUID()
|
|
4473
4503
|
};
|
|
4474
4504
|
if (isReply) {
|
|
4475
4505
|
body.in_reply_to_email_id = opts.reply;
|
|
@@ -4498,7 +4528,7 @@ var sendCommand = new Command68("send").description("Send an outbound email").re
|
|
|
4498
4528
|
});
|
|
4499
4529
|
|
|
4500
4530
|
// src/handwritten/commands/email/attachment.ts
|
|
4501
|
-
import { Command as
|
|
4531
|
+
import { Command as Command70 } from "commander";
|
|
4502
4532
|
import { createWriteStream } from "fs";
|
|
4503
4533
|
import { Readable } from "stream";
|
|
4504
4534
|
import { pipeline } from "stream/promises";
|
|
@@ -4516,7 +4546,7 @@ function parseContentDispositionFilename(header) {
|
|
|
4516
4546
|
}
|
|
4517
4547
|
|
|
4518
4548
|
// src/handwritten/commands/email/attachment.ts
|
|
4519
|
-
var attachmentCommand = new
|
|
4549
|
+
var attachmentCommand = new Command70("attachment").description("Download an inbound email attachment by index").argument("<email-id>").argument("<idx>").option("--output <path>", "output file path").option("--include-deleted", "allow downloads from soft-deleted parent emails").action(async (emailId, idxArg, opts) => {
|
|
4520
4550
|
const idx = Number(idxArg);
|
|
4521
4551
|
if (!Number.isInteger(idx) || idx < 0) {
|
|
4522
4552
|
process.stderr.write(`<idx> must be a non-negative integer (got "${idxArg}")
|
|
@@ -4548,8 +4578,8 @@ var attachmentCommand = new Command69("attachment").description("Download an inb
|
|
|
4548
4578
|
});
|
|
4549
4579
|
|
|
4550
4580
|
// src/handwritten/commands/drive/bind.ts
|
|
4551
|
-
import { Command as
|
|
4552
|
-
import { stat as
|
|
4581
|
+
import { Command as Command71 } from "commander";
|
|
4582
|
+
import { stat as stat3 } from "fs/promises";
|
|
4553
4583
|
import { resolve } from "path";
|
|
4554
4584
|
|
|
4555
4585
|
// src/handwritten/commands/drive/api.ts
|
|
@@ -4637,8 +4667,8 @@ async function createDriveApi(opts = {}) {
|
|
|
4637
4667
|
}
|
|
4638
4668
|
|
|
4639
4669
|
// src/handwritten/commands/drive/state.ts
|
|
4640
|
-
import { mkdir, open, readFile as readFile2, rename, rm, writeFile } from "fs/promises";
|
|
4641
|
-
import { randomUUID } from "crypto";
|
|
4670
|
+
import { mkdir, open, readFile as readFile2, rename, rm, stat as stat2, writeFile } from "fs/promises";
|
|
4671
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
4642
4672
|
import { join as join2 } from "path";
|
|
4643
4673
|
import { DateTime as DateTime4 } from "luxon";
|
|
4644
4674
|
|
|
@@ -4665,6 +4695,7 @@ function driveConflictTimestamp(clock = systemDriveClock) {
|
|
|
4665
4695
|
// src/handwritten/commands/drive/state.ts
|
|
4666
4696
|
var DRIVE_DIR = ".wspc-drive";
|
|
4667
4697
|
var STATE_FILE = "state.json";
|
|
4698
|
+
var STALE_LOCK_MS = 10 * 60 * 1e3;
|
|
4668
4699
|
function statePath(root) {
|
|
4669
4700
|
return join2(root, DRIVE_DIR, STATE_FILE);
|
|
4670
4701
|
}
|
|
@@ -4678,7 +4709,7 @@ async function readDriveState(root) {
|
|
|
4678
4709
|
}
|
|
4679
4710
|
async function writeDriveState(root, state, clock) {
|
|
4680
4711
|
await mkdir(join2(root, DRIVE_DIR), { recursive: true });
|
|
4681
|
-
const tmp = join2(root, DRIVE_DIR, `state.json.tmp-${process.pid}-${
|
|
4712
|
+
const tmp = join2(root, DRIVE_DIR, `state.json.tmp-${process.pid}-${randomUUID2()}`);
|
|
4682
4713
|
const snapshot = JSON.stringify(
|
|
4683
4714
|
{
|
|
4684
4715
|
...state,
|
|
@@ -4690,7 +4721,7 @@ async function writeDriveState(root, state, clock) {
|
|
|
4690
4721
|
const fullPath = statePath(root);
|
|
4691
4722
|
try {
|
|
4692
4723
|
await writeFile(tmp, snapshot, { mode: 384 });
|
|
4693
|
-
const fh = await open(tmp, "r");
|
|
4724
|
+
const fh = await open(tmp, "r+");
|
|
4694
4725
|
try {
|
|
4695
4726
|
await fh.sync();
|
|
4696
4727
|
} finally {
|
|
@@ -4734,7 +4765,7 @@ async function ensureDriveRealtimeState(root) {
|
|
|
4734
4765
|
...state,
|
|
4735
4766
|
realtime: {
|
|
4736
4767
|
...state.realtime,
|
|
4737
|
-
client_id: `drvcli_${
|
|
4768
|
+
client_id: `drvcli_${randomUUID2().replace(/-/g, "")}`
|
|
4738
4769
|
}
|
|
4739
4770
|
};
|
|
4740
4771
|
await writeDriveState(root, next);
|
|
@@ -4750,11 +4781,22 @@ async function writeDriveRealtimeState(root, realtime) {
|
|
|
4750
4781
|
async function withDriveLock(root, fn) {
|
|
4751
4782
|
await mkdir(join2(root, DRIVE_DIR), { recursive: true });
|
|
4752
4783
|
const lockFile = join2(root, DRIVE_DIR, "sync.lock");
|
|
4753
|
-
const fh = await open(lockFile, "wx").catch((error) => {
|
|
4754
|
-
if (error.code
|
|
4784
|
+
const fh = await open(lockFile, "wx").catch(async (error) => {
|
|
4785
|
+
if (error.code !== "EEXIST") throw error;
|
|
4786
|
+
const lockStat = await stat2(lockFile).catch((statError) => {
|
|
4787
|
+
if (statError.code === "ENOENT") return void 0;
|
|
4788
|
+
throw statError;
|
|
4789
|
+
});
|
|
4790
|
+
if (lockStat && Date.now() - lockStat.mtimeMs <= STALE_LOCK_MS) {
|
|
4755
4791
|
throw new Error("sync lock already exists");
|
|
4756
4792
|
}
|
|
4757
|
-
|
|
4793
|
+
await rm(lockFile, { force: true });
|
|
4794
|
+
return open(lockFile, "wx").catch((retryError) => {
|
|
4795
|
+
if (retryError.code === "EEXIST") {
|
|
4796
|
+
throw new Error("sync lock already exists");
|
|
4797
|
+
}
|
|
4798
|
+
throw retryError;
|
|
4799
|
+
});
|
|
4758
4800
|
});
|
|
4759
4801
|
try {
|
|
4760
4802
|
return await fn();
|
|
@@ -4802,7 +4844,7 @@ function isValidDriveState(value) {
|
|
|
4802
4844
|
async function assertExistingDirectory(path) {
|
|
4803
4845
|
let stats;
|
|
4804
4846
|
try {
|
|
4805
|
-
stats = await
|
|
4847
|
+
stats = await stat3(path);
|
|
4806
4848
|
} catch {
|
|
4807
4849
|
throw new Error(`local folder does not exist: ${path}`);
|
|
4808
4850
|
}
|
|
@@ -4811,7 +4853,7 @@ async function assertExistingDirectory(path) {
|
|
|
4811
4853
|
}
|
|
4812
4854
|
}
|
|
4813
4855
|
function driveBindCommand() {
|
|
4814
|
-
return new
|
|
4856
|
+
return new Command71("bind").description("Bind a local folder to an existing Drive library").requiredOption("--library <id>", "existing Drive library id").argument("[path]", "local folder path", ".").action(async (path, opts) => {
|
|
4815
4857
|
const root = resolve(path);
|
|
4816
4858
|
await assertExistingDirectory(root);
|
|
4817
4859
|
const api = await createDriveApi();
|
|
@@ -4829,10 +4871,10 @@ function driveBindCommand() {
|
|
|
4829
4871
|
}
|
|
4830
4872
|
|
|
4831
4873
|
// src/handwritten/commands/drive/sync.ts
|
|
4832
|
-
import { Command as
|
|
4874
|
+
import { Command as Command72 } from "commander";
|
|
4833
4875
|
import { mkdir as mkdir3, readFile as readFile4, rm as rm3, writeFile as writeFile3 } from "fs/promises";
|
|
4834
4876
|
import { basename as basename3, dirname as dirname2, join as join5, posix as pathPosix2, resolve as resolve3 } from "path";
|
|
4835
|
-
import { createHash as createHash3, randomUUID as
|
|
4877
|
+
import { createHash as createHash3, randomUUID as randomUUID4 } from "crypto";
|
|
4836
4878
|
|
|
4837
4879
|
// src/handwritten/commands/drive/decision.ts
|
|
4838
4880
|
function decideDriveAction(entry, local, remote) {
|
|
@@ -5213,7 +5255,7 @@ async function hashDriveFile(path) {
|
|
|
5213
5255
|
import { createWriteStream as createWriteStream2 } from "fs";
|
|
5214
5256
|
import { link, mkdir as mkdir2, readFile as readFile3, rename as rename2, rm as rm2, unlink, writeFile as writeFile2 } from "fs/promises";
|
|
5215
5257
|
import { basename as basename2, dirname, join as join4 } from "path";
|
|
5216
|
-
import { createHash as createHash2, randomUUID as
|
|
5258
|
+
import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
|
|
5217
5259
|
import { Readable as Readable2, Transform } from "stream";
|
|
5218
5260
|
import { pipeline as pipeline2 } from "stream/promises";
|
|
5219
5261
|
async function assertLocalStillScanned(localPath, scanned) {
|
|
@@ -5228,7 +5270,7 @@ async function assertLocalStillScanned(localPath, scanned) {
|
|
|
5228
5270
|
async function writeMergedLocalFile(root, path, bytes, digest, scanned, onLocalMutation) {
|
|
5229
5271
|
const target = resolveInsideRoot(root, path);
|
|
5230
5272
|
await mkdir2(dirname(target), { recursive: true });
|
|
5231
|
-
const tmp = join4(dirname(target), `.${basename2(target)}.wspc-merge-${
|
|
5273
|
+
const tmp = join4(dirname(target), `.${basename2(target)}.wspc-merge-${randomUUID3()}.tmp`);
|
|
5232
5274
|
try {
|
|
5233
5275
|
await writeFile2(tmp, bytes, { flag: "wx" });
|
|
5234
5276
|
return await installMergedLocalFile(root, path, tmp, scanned, digest, bytes.byteLength, onLocalMutation);
|
|
@@ -5280,7 +5322,7 @@ async function installMergedLocalFile(root, path, tmp, scanned, mergedSha256, me
|
|
|
5280
5322
|
}
|
|
5281
5323
|
}
|
|
5282
5324
|
async function restoreMergedLocalFile(target, backup, backupSha256, backupSizeBytes, mergedSha256, mergedSizeBytes) {
|
|
5283
|
-
const quarantine = join4(dirname(target), `.${basename2(target)}.wspc-merge-restore-${
|
|
5325
|
+
const quarantine = join4(dirname(target), `.${basename2(target)}.wspc-merge-restore-${randomUUID3()}.tmp`);
|
|
5284
5326
|
try {
|
|
5285
5327
|
await rename2(target, quarantine);
|
|
5286
5328
|
} catch (error) {
|
|
@@ -5310,7 +5352,7 @@ async function restoreMergedLocalFile(target, backup, backupSha256, backupSizeBy
|
|
|
5310
5352
|
async function downloadRemote(root, libraryId, path, api, expectedSha256, entry, onLocalMutation) {
|
|
5311
5353
|
const target = resolveInsideRoot(root, path);
|
|
5312
5354
|
await mkdir2(dirname(target), { recursive: true });
|
|
5313
|
-
const tmp = join4(dirname(target), `.${basename2(target)}.wspc-download-${
|
|
5355
|
+
const tmp = join4(dirname(target), `.${basename2(target)}.wspc-download-${randomUUID3()}.tmp`);
|
|
5314
5356
|
try {
|
|
5315
5357
|
const response = await api.downloadFile(libraryId, path);
|
|
5316
5358
|
if (!response.body) {
|
|
@@ -5444,7 +5486,7 @@ async function localFileExists(path) {
|
|
|
5444
5486
|
return digest !== void 0;
|
|
5445
5487
|
}
|
|
5446
5488
|
function localMutationBackupPath(target) {
|
|
5447
|
-
return join4(dirname(target), `.${basename2(target)}.wspc-backup-${
|
|
5489
|
+
return join4(dirname(target), `.${basename2(target)}.wspc-backup-${randomUUID3()}.tmp`);
|
|
5448
5490
|
}
|
|
5449
5491
|
function expectedLocalBaseSha256(entry) {
|
|
5450
5492
|
return entry?.last_local_sha256 ?? entry?.content_sha256;
|
|
@@ -5546,7 +5588,7 @@ async function runDriveSyncOnce(root, api, clock = systemDriveClock) {
|
|
|
5546
5588
|
});
|
|
5547
5589
|
}
|
|
5548
5590
|
function driveSyncCommand(api) {
|
|
5549
|
-
const sync = new
|
|
5591
|
+
const sync = new Command72("sync").description("Drive sync commands");
|
|
5550
5592
|
sync.command("once").description("Run one Drive sync pass").argument("[path]", "local folder path", ".").action(async (path) => {
|
|
5551
5593
|
const summary = await runDriveSyncOnce(resolve3(path), api);
|
|
5552
5594
|
render({ kind: "drive_sync_once", display: { shape: "object" } }, summary);
|
|
@@ -5858,7 +5900,7 @@ async function writeConflictCopy(root, path, side, versionId, bytes, clock) {
|
|
|
5858
5900
|
const target = resolveInsideRoot(root, candidate);
|
|
5859
5901
|
await mkdir3(dirname2(target), { recursive: true });
|
|
5860
5902
|
for (; ; ) {
|
|
5861
|
-
const tmp = join5(dirname2(target), `.${basename3(target)}.wspc-conflict-${
|
|
5903
|
+
const tmp = join5(dirname2(target), `.${basename3(target)}.wspc-conflict-${randomUUID4()}.tmp`);
|
|
5862
5904
|
let tmpWritten = false;
|
|
5863
5905
|
try {
|
|
5864
5906
|
await writeFile3(tmp, bytes, { flag: "wx" });
|
|
@@ -5999,7 +6041,7 @@ function containsVersionConflict(value) {
|
|
|
5999
6041
|
}
|
|
6000
6042
|
|
|
6001
6043
|
// src/handwritten/commands/drive/watch.ts
|
|
6002
|
-
import { Command as
|
|
6044
|
+
import { Command as Command73 } from "commander";
|
|
6003
6045
|
import chokidar from "chokidar";
|
|
6004
6046
|
import { relative as relative2, resolve as resolve4 } from "path";
|
|
6005
6047
|
|
|
@@ -6213,9 +6255,20 @@ function redactedRealtimeError(error) {
|
|
|
6213
6255
|
return "auth failed";
|
|
6214
6256
|
}
|
|
6215
6257
|
if (/\bnetwork|fetch|close\b/i.test(text)) {
|
|
6216
|
-
return "network error";
|
|
6258
|
+
return withRealtimeCode("network error", error);
|
|
6259
|
+
}
|
|
6260
|
+
return withRealtimeCode("realtime error", error);
|
|
6261
|
+
}
|
|
6262
|
+
function withRealtimeCode(label, error) {
|
|
6263
|
+
if (typeof error !== "object" || error === null) return label;
|
|
6264
|
+
const code = error.code;
|
|
6265
|
+
if (typeof code === "string" && /^[A-Z][A-Z0-9_]{1,31}$/.test(code)) {
|
|
6266
|
+
return `${label} (${code})`;
|
|
6267
|
+
}
|
|
6268
|
+
if (typeof code === "number" && Number.isInteger(code)) {
|
|
6269
|
+
return `${label} (${code})`;
|
|
6217
6270
|
}
|
|
6218
|
-
return
|
|
6271
|
+
return label;
|
|
6219
6272
|
}
|
|
6220
6273
|
function isRecord3(value) {
|
|
6221
6274
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -6438,7 +6491,7 @@ async function runDriveWatch(root, options = {}) {
|
|
|
6438
6491
|
}
|
|
6439
6492
|
}
|
|
6440
6493
|
function driveWatchCommand(options = {}) {
|
|
6441
|
-
return new
|
|
6494
|
+
return new Command73("watch").description("Watch a bound Drive folder and sync local changes").argument("[path]", "local folder path", ".").action(async (path) => {
|
|
6442
6495
|
await runDriveWatch(resolve4(path), options);
|
|
6443
6496
|
});
|
|
6444
6497
|
}
|
|
@@ -6502,7 +6555,7 @@ function waitForStopSignal(onRegistered) {
|
|
|
6502
6555
|
function mountDriveCommands(program) {
|
|
6503
6556
|
let drive = program.commands.find((c) => c.name() === "drive");
|
|
6504
6557
|
if (!drive) {
|
|
6505
|
-
drive = new
|
|
6558
|
+
drive = new Command74("drive").description("Drive commands");
|
|
6506
6559
|
program.addCommand(drive);
|
|
6507
6560
|
}
|
|
6508
6561
|
if (!drive.commands.some((c) => c.name() === "bind")) {
|
|
@@ -6528,7 +6581,7 @@ function isCliEntrypoint(argv = process.argv, metaUrl = import.meta.url) {
|
|
|
6528
6581
|
}
|
|
6529
6582
|
}
|
|
6530
6583
|
function buildProgram() {
|
|
6531
|
-
const program = new
|
|
6584
|
+
const program = new Command74().name("wspc").description("Official CLI for wspc.ai").version(`wspc ${VERSION} (spec ${SPEC_SHA}, fetched ${SPEC_FETCHED_AT})`).option("--json", "Output raw JSON (machine-readable)").option("--account <email>", "Run as a specific account (overrides the active account)").hook("preAction", (_thisCommand, actionCommand) => {
|
|
6532
6585
|
const globals = actionCommand.optsWithGlobals();
|
|
6533
6586
|
if (globals.json) process.env.WSPC_OUTPUT = "json";
|
|
6534
6587
|
if (globals.account) process.env.WSPC_ACCOUNT = String(globals.account);
|