ai-project-manage-cli 6.0.92 → 6.0.94

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 (2) hide show
  1. package/dist/index.js +356 -175
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { Command } from "commander";
5
- import { AssumptionPlanSource } from "@opc/shared";
6
5
 
7
6
  // src/config.ts
8
7
  import { mkdirSync, readFileSync, writeFileSync } from "fs";
@@ -2217,6 +2216,7 @@ async function runUpdateMessageStatus(options) {
2217
2216
 
2218
2217
  // src/commands/connect.ts
2219
2218
  import WebSocket from "ws";
2219
+ import { setTimeout as delay2 } from "node:timers/promises";
2220
2220
 
2221
2221
  // src/ws/protocol.ts
2222
2222
  function nonEmptyString(v) {
@@ -3655,6 +3655,7 @@ function forceReleaseConnectLock() {
3655
3655
 
3656
3656
  // src/commands/daemon.ts
3657
3657
  import { spawnSync as spawnSync2 } from "child_process";
3658
+ import { setTimeout as delay } from "node:timers/promises";
3658
3659
  import { createRequire } from "node:module";
3659
3660
  import { existsSync as existsSync14, mkdirSync as mkdirSync7, writeFileSync as writeFileSync13 } from "fs";
3660
3661
  import { join as join15 } from "path";
@@ -3683,10 +3684,12 @@ function buildConnectPm2Ecosystem(options) {
3683
3684
  args: options.connectArgs,
3684
3685
  autorestart: true,
3685
3686
  min_uptime: "10s",
3686
- max_restarts: 100,
3687
+ max_restarts: 0,
3687
3688
  restart_delay: 3e3,
3688
3689
  exp_backoff_restart_delay: 1e3,
3689
3690
  max_memory_restart: "1G",
3691
+ kill_timeout: 5e3,
3692
+ shutdown_with_message: true,
3690
3693
  env
3691
3694
  }
3692
3695
  ]
@@ -3879,10 +3882,44 @@ async function runDaemonStart(options) {
3879
3882
  console.log("[apm] \u505C\u6B62: apm daemon stop");
3880
3883
  }
3881
3884
  async function runDaemonStop() {
3885
+ pruneStaleConnectLock();
3886
+ if (!isPm2ConnectOnline()) {
3887
+ killConnectLockProcessIfAlive();
3888
+ forceReleaseConnectLock();
3889
+ console.log(`[apm] ${PM2_APP_NAME} \u672A\u5728\u8FD0\u884C`);
3890
+ return;
3891
+ }
3882
3892
  runPm2(["stop", PM2_APP_NAME], { inherit: true });
3893
+ for (let i = 0; i < 10; i += 1) {
3894
+ if (!isPm2ConnectOnline()) break;
3895
+ await delay(500);
3896
+ }
3897
+ if (isPm2ConnectOnline()) {
3898
+ console.log(`[apm] \u4F18\u96C5\u505C\u6B62\u8D85\u65F6\uFF0C\u5F3A\u5236\u79FB\u9664 ${PM2_APP_NAME}\u2026`);
3899
+ runPm2(["delete", PM2_APP_NAME], { inherit: true });
3900
+ }
3901
+ killConnectLockProcessIfAlive();
3883
3902
  forceReleaseConnectLock();
3884
3903
  console.log(`[apm] ${PM2_APP_NAME} \u5DF2\u505C\u6B62`);
3885
3904
  }
3905
+ function killConnectLockProcessIfAlive() {
3906
+ pruneStaleConnectLock();
3907
+ const lock = readConnectLock();
3908
+ if (!lock || !isProcessAlive(lock.pid)) return;
3909
+ try {
3910
+ process.kill(lock.pid, "SIGTERM");
3911
+ } catch {
3912
+ forceReleaseConnectLock();
3913
+ return;
3914
+ }
3915
+ if (isProcessAlive(lock.pid)) {
3916
+ try {
3917
+ process.kill(lock.pid, "SIGKILL");
3918
+ } catch {
3919
+ }
3920
+ }
3921
+ forceReleaseConnectLock();
3922
+ }
3886
3923
  async function runDaemonRestart(options) {
3887
3924
  pruneStaleConnectLock();
3888
3925
  const lock = readConnectLock();
@@ -3917,6 +3954,8 @@ async function runDaemonLogs(options) {
3917
3954
 
3918
3955
  // src/commands/connect.ts
3919
3956
  var HEARTBEAT_MS = 3e4;
3957
+ var RECONNECT_MIN_MS = 1e3;
3958
+ var RECONNECT_MAX_MS = 6e4;
3920
3959
  async function updateMessageStatus(cfg, messageId, status) {
3921
3960
  const api = createApmApiClient(cfg);
3922
3961
  await api.cli.updateMessageStatus({ id: messageId, status });
@@ -4069,6 +4108,17 @@ async function handleInboundMessage(cfg, msg, signal, ctx) {
4069
4108
  }
4070
4109
  }
4071
4110
  }
4111
+ function interruptibleSleep(ms, signal) {
4112
+ if (signal.aborted) return Promise.resolve();
4113
+ return new Promise((resolve5) => {
4114
+ const timer = setTimeout(resolve5, ms);
4115
+ const onAbort = () => {
4116
+ clearTimeout(timer);
4117
+ resolve5();
4118
+ };
4119
+ signal.addEventListener("abort", onAbort, { once: true });
4120
+ });
4121
+ }
4072
4122
  function startHeartbeat(ws, clientMachineId) {
4073
4123
  const send = () => {
4074
4124
  if (ws.readyState === WebSocket.OPEN) {
@@ -4084,182 +4134,274 @@ function startHeartbeat(ws, clientMachineId) {
4084
4134
  const timer = setInterval(send, HEARTBEAT_MS);
4085
4135
  return () => clearInterval(timer);
4086
4136
  }
4087
- async function runConnect(options) {
4088
- const { didUpdate } = await runUpdate();
4089
- if (didUpdate) {
4090
- await handleConnectAfterUpdate(options);
4091
- }
4092
- const cfg = await ensureLoggedConfig();
4093
- if (options.server?.trim()) {
4094
- cfg.baseUrl = options.server.trim().replace(/\/+$/, "");
4095
- }
4096
- const clientMachineId = resolveClientMachineId(cfg);
4097
- if (!clientMachineId) {
4098
- console.error("[apm] config \u7F3A\u5C11 clientMachineId\uFF0C\u8BF7\u91CD\u65B0 apm login");
4099
- process.exit(1);
4100
- }
4101
- assertConnectNotRunning();
4102
- acquireConnectLock("foreground");
4103
- process.on("exit", releaseConnectLock);
4104
- const url = buildAgentWsUrl(cfg.baseUrl, resolveApiKey(cfg));
4105
- console.log(`[apm] \u8FDE\u63A5 ${cfg.baseUrl} \u2026`);
4106
- await new Promise((resolve5, reject) => {
4107
- const ws = new WebSocket(url);
4108
- let stopHeartbeat;
4109
- let shuttingDown = false;
4110
- const shutdownAbort = new AbortController();
4111
- const runSlots = createRunSlotPool();
4112
- const activeTasks = /* @__PURE__ */ new Set();
4113
- const activeRuns = /* @__PURE__ */ new Map();
4114
- const pendingCancels = /* @__PURE__ */ new Set();
4115
- const shutdown = async (code = 0) => {
4116
- if (shuttingDown) return;
4117
- shuttingDown = true;
4118
- logAbortSignalStats(
4119
- shutdownAbort.signal,
4120
- "connect:shutdown-before-abort"
4121
- );
4122
- shutdownAbort.abort();
4123
- logAbortSignalStats(shutdownAbort.signal, "connect:shutdown-after-abort");
4124
- stopHeartbeat?.();
4125
- if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
4126
- ws.terminate();
4127
- }
4128
- try {
4129
- await Promise.race([
4130
- Promise.all(activeTasks),
4131
- new Promise((r) => setTimeout(r, SHUTDOWN_DRAIN_MS))
4132
- ]);
4133
- } catch {
4134
- }
4135
- releaseConnectLock();
4136
- resolve5();
4137
- process.exit(code);
4138
- };
4139
- ws.on("open", () => {
4140
- console.log("[apm] WebSocket \u5DF2\u8FDE\u63A5");
4141
- stopHeartbeat = startHeartbeat(ws, clientMachineId);
4142
- });
4143
- ws.on("message", (data) => {
4144
- if (shuttingDown) return;
4145
- const text = Buffer.isBuffer(data) ? data.toString("utf8") : String(data);
4146
- const parsed = parseAgentWsMessage(text);
4147
- if (parsed === null) {
4148
- console.error("[apm] \u6536\u5230\u65E0\u6548 JSON");
4149
- return;
4150
- }
4151
- if (typeof parsed === "object" && parsed !== null && parsed.type === "heartbeat") {
4152
- return;
4153
- }
4154
- const validated = validateAgentWsMessage(parsed, "outbound");
4155
- if (!validated.ok) {
4156
- console.error(`[apm] \u6536\u5230\u65E0\u6548 WS \u5305: ${validated.reason}`);
4157
- return;
4158
- }
4159
- if (validated.data.type === "cancel") {
4160
- const { messageId } = validated.data;
4161
- pendingCancels.add(messageId);
4162
- activeRuns.get(messageId)?.abort();
4163
- return;
4164
- }
4165
- if (validated.data.type === "deploy") {
4166
- const msg2 = validated.data;
4167
- const perDeployController = new AbortController();
4168
- const signal2 = AbortSignal.any([
4169
- shutdownAbort.signal,
4170
- perDeployController.signal
4171
- ]);
4172
- const task2 = (async () => {
4173
- await runSlots.acquire();
4174
- try {
4175
- await handleInboundDeploy(cfg, msg2, signal2);
4176
- } finally {
4177
- runSlots.release();
4178
- }
4179
- })();
4180
- activeTasks.add(task2);
4181
- void task2.finally(() => {
4182
- activeTasks.delete(task2);
4183
- });
4184
- return;
4185
- }
4186
- if (validated.data.type !== "message") {
4187
- return;
4188
- }
4189
- const msg = validated.data;
4190
- const perMessageController = new AbortController();
4191
- activeRuns.set(msg.messageId, perMessageController);
4192
- if (pendingCancels.has(msg.messageId)) {
4193
- activeRuns.delete(msg.messageId);
4194
- pendingCancels.delete(msg.messageId);
4195
- return;
4196
- }
4197
- const signal = AbortSignal.any([
4137
+ function attachWsHandlers(ws, ctx, onOpen) {
4138
+ const {
4139
+ cfg,
4140
+ clientMachineId,
4141
+ shutdownAbort,
4142
+ runSlots,
4143
+ activeTasks,
4144
+ activeRuns,
4145
+ pendingCancels,
4146
+ isShuttingDown
4147
+ } = ctx;
4148
+ ws.on("open", () => {
4149
+ console.log("[apm] WebSocket \u5DF2\u8FDE\u63A5");
4150
+ onOpen();
4151
+ });
4152
+ ws.on("message", (data) => {
4153
+ if (isShuttingDown()) return;
4154
+ const text = Buffer.isBuffer(data) ? data.toString("utf8") : String(data);
4155
+ const parsed = parseAgentWsMessage(text);
4156
+ if (parsed === null) {
4157
+ console.error("[apm] \u6536\u5230\u65E0\u6548 JSON");
4158
+ return;
4159
+ }
4160
+ if (typeof parsed === "object" && parsed !== null && parsed.type === "heartbeat") {
4161
+ return;
4162
+ }
4163
+ const validated = validateAgentWsMessage(parsed, "outbound");
4164
+ if (!validated.ok) {
4165
+ console.error(`[apm] \u6536\u5230\u65E0\u6548 WS \u5305: ${validated.reason}`);
4166
+ return;
4167
+ }
4168
+ if (validated.data.type === "cancel") {
4169
+ const { messageId } = validated.data;
4170
+ pendingCancels.add(messageId);
4171
+ activeRuns.get(messageId)?.abort();
4172
+ return;
4173
+ }
4174
+ if (validated.data.type === "deploy") {
4175
+ const msg2 = validated.data;
4176
+ const perDeployController = new AbortController();
4177
+ const signal2 = AbortSignal.any([
4198
4178
  shutdownAbort.signal,
4199
- perMessageController.signal
4179
+ perDeployController.signal
4200
4180
  ]);
4201
- const ctx = {
4202
- shutdownSignal: shutdownAbort.signal,
4203
- perMessageSignal: perMessageController.signal
4204
- };
4205
- const task = (async () => {
4181
+ const task2 = (async () => {
4206
4182
  await runSlots.acquire();
4207
4183
  try {
4208
- if (signal.aborted || isUserCancelled(ctx)) return;
4209
- try {
4210
- await updateMessageStatus(cfg, msg.messageId, "TYPING");
4211
- } catch (typingErr) {
4212
- if (isUserCancelled(ctx)) return;
4213
- console.error(
4214
- "[apm] \u66F4\u65B0 TYPING \u72B6\u6001\u5931\u8D25:",
4215
- typingErr instanceof Error ? typingErr.message : typingErr
4216
- );
4217
- try {
4218
- await setMessageError(
4219
- cfg,
4220
- msg.messageId,
4221
- typingErr instanceof Error ? typingErr.message : String(typingErr)
4222
- );
4223
- await updateMessageStatus(cfg, msg.messageId, "FAILED");
4224
- } catch (statusErr) {
4225
- console.error(
4226
- "[apm] \u66F4\u65B0 FAILED \u72B6\u6001\u5931\u8D25:",
4227
- statusErr instanceof Error ? statusErr.message : statusErr
4228
- );
4229
- }
4230
- return;
4231
- }
4232
- await handleInboundMessage(cfg, msg, signal, ctx);
4184
+ await handleInboundDeploy(cfg, msg2, signal2);
4233
4185
  } finally {
4234
4186
  runSlots.release();
4235
- activeRuns.delete(msg.messageId);
4236
- pendingCancels.delete(msg.messageId);
4237
4187
  }
4238
4188
  })();
4239
- activeTasks.add(task);
4240
- void task.finally(() => {
4241
- activeTasks.delete(task);
4189
+ activeTasks.add(task2);
4190
+ void task2.finally(() => {
4191
+ activeTasks.delete(task2);
4242
4192
  });
4193
+ return;
4194
+ }
4195
+ if (validated.data.type !== "message") {
4196
+ return;
4197
+ }
4198
+ const msg = validated.data;
4199
+ const perMessageController = new AbortController();
4200
+ activeRuns.set(msg.messageId, perMessageController);
4201
+ if (pendingCancels.has(msg.messageId)) {
4202
+ activeRuns.delete(msg.messageId);
4203
+ pendingCancels.delete(msg.messageId);
4204
+ return;
4205
+ }
4206
+ const signal = AbortSignal.any([
4207
+ shutdownAbort.signal,
4208
+ perMessageController.signal
4209
+ ]);
4210
+ const messageCtx = {
4211
+ shutdownSignal: shutdownAbort.signal,
4212
+ perMessageSignal: perMessageController.signal
4213
+ };
4214
+ const task = (async () => {
4215
+ await runSlots.acquire();
4216
+ try {
4217
+ if (signal.aborted || isUserCancelled(messageCtx)) return;
4218
+ try {
4219
+ await updateMessageStatus(cfg, msg.messageId, "TYPING");
4220
+ } catch (typingErr) {
4221
+ if (isUserCancelled(messageCtx)) return;
4222
+ console.error(
4223
+ "[apm] \u66F4\u65B0 TYPING \u72B6\u6001\u5931\u8D25:",
4224
+ typingErr instanceof Error ? typingErr.message : typingErr
4225
+ );
4226
+ try {
4227
+ await setMessageError(
4228
+ cfg,
4229
+ msg.messageId,
4230
+ typingErr instanceof Error ? typingErr.message : String(typingErr)
4231
+ );
4232
+ await updateMessageStatus(cfg, msg.messageId, "FAILED");
4233
+ } catch (statusErr) {
4234
+ console.error(
4235
+ "[apm] \u66F4\u65B0 FAILED \u72B6\u6001\u5931\u8D25:",
4236
+ statusErr instanceof Error ? statusErr.message : statusErr
4237
+ );
4238
+ }
4239
+ return;
4240
+ }
4241
+ await handleInboundMessage(cfg, msg, signal, messageCtx);
4242
+ } finally {
4243
+ runSlots.release();
4244
+ activeRuns.delete(msg.messageId);
4245
+ pendingCancels.delete(msg.messageId);
4246
+ }
4247
+ })();
4248
+ activeTasks.add(task);
4249
+ void task.finally(() => {
4250
+ activeTasks.delete(task);
4251
+ });
4252
+ });
4253
+ }
4254
+ function connectOnce(url, ctx, connectionAbort, onConnected) {
4255
+ return new Promise((resolve5, reject) => {
4256
+ const ws = new WebSocket(url);
4257
+ let stopHeartbeat;
4258
+ let settled = false;
4259
+ const finish = (fn) => {
4260
+ if (settled) return;
4261
+ settled = true;
4262
+ connectionAbort.removeEventListener("abort", onConnectionAbort);
4263
+ stopHeartbeat?.();
4264
+ fn();
4265
+ };
4266
+ const onConnectionAbort = () => {
4267
+ if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
4268
+ ws.terminate();
4269
+ }
4270
+ };
4271
+ connectionAbort.addEventListener("abort", onConnectionAbort);
4272
+ attachWsHandlers(ws, ctx, () => {
4273
+ stopHeartbeat = startHeartbeat(ws, ctx.clientMachineId);
4274
+ onConnected();
4243
4275
  });
4244
4276
  ws.on("close", (code, reason) => {
4245
4277
  console.log(
4246
4278
  `[apm] \u8FDE\u63A5\u5DF2\u65AD\u5F00 code=${code}${reason ? ` reason=${reason.toString()}` : ""}`
4247
4279
  );
4248
- void shutdown();
4280
+ if (ctx.isShuttingDown()) {
4281
+ finish(() => reject(new Error("shutdown")));
4282
+ return;
4283
+ }
4284
+ finish(resolve5);
4249
4285
  });
4250
4286
  ws.on("error", (err) => {
4251
4287
  console.error("[apm] WebSocket \u9519\u8BEF:", err.message);
4252
- reject(err);
4253
- });
4254
- process.on("SIGINT", () => {
4255
- console.log("[apm] \u6B63\u5728\u5173\u95ED\u2026");
4256
- void shutdown();
4257
- });
4258
- process.on("SIGTERM", () => {
4259
- void shutdown();
4288
+ if (ctx.isShuttingDown()) {
4289
+ finish(() => reject(new Error("shutdown")));
4290
+ return;
4291
+ }
4292
+ if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
4293
+ ws.terminate();
4294
+ }
4260
4295
  });
4261
4296
  });
4262
4297
  }
4298
+ async function runConnect(options) {
4299
+ const { didUpdate } = await runUpdate();
4300
+ if (didUpdate) {
4301
+ await handleConnectAfterUpdate(options);
4302
+ }
4303
+ const cfg = await ensureLoggedConfig();
4304
+ if (options.server?.trim()) {
4305
+ cfg.baseUrl = options.server.trim().replace(/\/+$/, "");
4306
+ }
4307
+ const clientMachineId = resolveClientMachineId(cfg);
4308
+ if (!clientMachineId) {
4309
+ console.error("[apm] config \u7F3A\u5C11 clientMachineId\uFF0C\u8BF7\u91CD\u65B0 apm login");
4310
+ process.exit(1);
4311
+ }
4312
+ assertConnectNotRunning();
4313
+ const lockMode = isRunningUnderPm2() ? "pm2" : "foreground";
4314
+ acquireConnectLock(lockMode);
4315
+ process.on("exit", releaseConnectLock);
4316
+ let shuttingDown = false;
4317
+ let shutdownPromise = null;
4318
+ const lifecycleAbort = new AbortController();
4319
+ const shutdownAbort = new AbortController();
4320
+ const runSlots = createRunSlotPool();
4321
+ const activeTasks = /* @__PURE__ */ new Set();
4322
+ const activeRuns = /* @__PURE__ */ new Map();
4323
+ const pendingCancels = /* @__PURE__ */ new Set();
4324
+ let currentConnectionAbort = null;
4325
+ const sessionCtx = {
4326
+ cfg,
4327
+ clientMachineId,
4328
+ shutdownAbort,
4329
+ runSlots,
4330
+ activeTasks,
4331
+ activeRuns,
4332
+ pendingCancels,
4333
+ isShuttingDown: () => shuttingDown
4334
+ };
4335
+ const drainAndExit = async (code = 0) => {
4336
+ const drainMs = isRunningUnderPm2() ? 500 : SHUTDOWN_DRAIN_MS;
4337
+ lifecycleAbort.abort();
4338
+ currentConnectionAbort?.abort();
4339
+ logAbortSignalStats(shutdownAbort.signal, "connect:shutdown-before-abort");
4340
+ shutdownAbort.abort();
4341
+ logAbortSignalStats(shutdownAbort.signal, "connect:shutdown-after-abort");
4342
+ try {
4343
+ await Promise.race([Promise.all(activeTasks), delay2(drainMs)]);
4344
+ } catch {
4345
+ }
4346
+ releaseConnectLock();
4347
+ process.exit(code);
4348
+ };
4349
+ const shutdown = (code = 0) => {
4350
+ if (shuttingDown) return shutdownPromise ?? Promise.resolve();
4351
+ shuttingDown = true;
4352
+ shutdownPromise = drainAndExit(code);
4353
+ return shutdownPromise;
4354
+ };
4355
+ const onStopSignal = (code = 0) => {
4356
+ console.log("[apm] \u6B63\u5728\u5173\u95ED\u2026");
4357
+ void shutdown(code);
4358
+ };
4359
+ process.on("SIGINT", () => onStopSignal(0));
4360
+ process.on("SIGTERM", () => onStopSignal(0));
4361
+ if (isRunningUnderPm2()) {
4362
+ process.on("message", (msg) => {
4363
+ if (msg === "shutdown") onStopSignal(0);
4364
+ });
4365
+ }
4366
+ let reconnectDelay = RECONNECT_MIN_MS;
4367
+ while (!shuttingDown) {
4368
+ currentConnectionAbort = new AbortController();
4369
+ const connectionSignal = AbortSignal.any([
4370
+ lifecycleAbort.signal,
4371
+ currentConnectionAbort.signal
4372
+ ]);
4373
+ const url = buildAgentWsUrl(cfg.baseUrl, resolveApiKey(cfg));
4374
+ console.log(`[apm] \u8FDE\u63A5 ${cfg.baseUrl} \u2026`);
4375
+ try {
4376
+ await connectOnce(url, sessionCtx, connectionSignal, () => {
4377
+ reconnectDelay = RECONNECT_MIN_MS;
4378
+ });
4379
+ if (shuttingDown) break;
4380
+ console.log(`[apm] ${reconnectDelay}ms \u540E\u5C1D\u8BD5\u91CD\u8FDE\u2026`);
4381
+ await interruptibleSleep(reconnectDelay, lifecycleAbort.signal);
4382
+ if (shuttingDown) break;
4383
+ reconnectDelay = Math.min(reconnectDelay * 2, RECONNECT_MAX_MS);
4384
+ } catch (err) {
4385
+ if (shuttingDown) break;
4386
+ const detail = err instanceof Error ? err.message : String(err);
4387
+ if (detail === "shutdown") break;
4388
+ console.error("[apm] \u8FDE\u63A5\u5931\u8D25:", detail);
4389
+ console.log(`[apm] ${reconnectDelay}ms \u540E\u5C1D\u8BD5\u91CD\u8FDE\u2026`);
4390
+ await interruptibleSleep(reconnectDelay, lifecycleAbort.signal);
4391
+ if (shuttingDown) break;
4392
+ reconnectDelay = Math.min(reconnectDelay * 2, RECONNECT_MAX_MS);
4393
+ } finally {
4394
+ currentConnectionAbort = null;
4395
+ }
4396
+ }
4397
+ if (!shuttingDown) {
4398
+ void shutdown(0);
4399
+ return;
4400
+ }
4401
+ if (shutdownPromise) {
4402
+ await shutdownPromise;
4403
+ }
4404
+ }
4263
4405
 
4264
4406
  // src/commands/create-pr-errors.ts
4265
4407
  import { ApiError as ApiError2 } from "listpage-http";
@@ -4789,6 +4931,56 @@ async function zipDirectory(distDir, zipPath) {
4789
4931
  console.error(`\u5DF2\u751F\u6210: ${zipPath} (${sizeMb} MB)`);
4790
4932
  return content.length;
4791
4933
  }
4934
+ var SFTP_UPLOAD_MAX_ATTEMPTS = 3;
4935
+ var SFTP_FAST_PUT_OPTIONS = {
4936
+ chunkSize: 64 * 1024,
4937
+ concurrency: 4
4938
+ };
4939
+ function buildSftpConnectOptions(settings) {
4940
+ return {
4941
+ host: settings.host,
4942
+ port: settings.port,
4943
+ username: settings.username,
4944
+ password: settings.password,
4945
+ readyTimeout: 3e4,
4946
+ tryKeyboard: true,
4947
+ keepaliveInterval: 1e4,
4948
+ keepaliveCountMax: 3
4949
+ };
4950
+ }
4951
+ async function sleep(ms) {
4952
+ await new Promise((resolve5) => setTimeout(resolve5, ms));
4953
+ }
4954
+ async function uploadZipWithRetry(settings, localZip, remoteZipPath) {
4955
+ let lastError;
4956
+ for (let attempt = 1; attempt <= SFTP_UPLOAD_MAX_ATTEMPTS; attempt++) {
4957
+ const sftp = new SftpClient();
4958
+ try {
4959
+ if (attempt > 1) {
4960
+ console.error(
4961
+ `SFTP \u4E0A\u4F20\u91CD\u8BD5 (${attempt}/${SFTP_UPLOAD_MAX_ATTEMPTS})...`
4962
+ );
4963
+ }
4964
+ await sftp.connect(buildSftpConnectOptions(settings));
4965
+ await ensureRemoteDir(sftp, settings.remotePath);
4966
+ await sftp.fastPut(localZip, remoteZipPath, SFTP_FAST_PUT_OPTIONS);
4967
+ return sftp;
4968
+ } catch (err) {
4969
+ lastError = err;
4970
+ try {
4971
+ await sftp.end();
4972
+ } catch {
4973
+ }
4974
+ if (attempt < SFTP_UPLOAD_MAX_ATTEMPTS) {
4975
+ const message = err instanceof Error ? err.message : String(err);
4976
+ const delaySec = attempt * 2;
4977
+ console.error(`SFTP \u4E0A\u4F20\u5931\u8D25 (${message})\uFF0C${delaySec}s \u540E\u91CD\u8BD5...`);
4978
+ await sleep(delaySec * 1e3);
4979
+ }
4980
+ }
4981
+ }
4982
+ throw lastError;
4983
+ }
4792
4984
  async function ensureRemoteDir(sftp, dir) {
4793
4985
  const parts = dir.replace(/\\/g, "/").split("/").filter(Boolean);
4794
4986
  let current = dir.startsWith("/") ? "" : ".";
@@ -4848,18 +5040,8 @@ async function uploadAndMaybeExtract(settings, localZip, extract) {
4848
5040
  console.error(
4849
5041
  `\u8FDE\u63A5 ${settings.username}@${settings.host}:${settings.port} ...`
4850
5042
  );
4851
- const sftp = new SftpClient();
5043
+ const sftp = await uploadZipWithRetry(settings, localZip, remoteZipPath);
4852
5044
  try {
4853
- await sftp.connect({
4854
- host: settings.host,
4855
- port: settings.port,
4856
- username: settings.username,
4857
- password: settings.password,
4858
- readyTimeout: 2e4,
4859
- tryKeyboard: true
4860
- });
4861
- await ensureRemoteDir(sftp, settings.remotePath);
4862
- await sftp.put(localZip, remoteZipPath);
4863
5045
  console.error(` \u2713 ${localZip} -> ${remoteZipPath}`);
4864
5046
  if (extract) {
4865
5047
  const target = settings.remotePath.replace(/\/$/, "");
@@ -7006,15 +7188,14 @@ function buildProgram() {
7006
7188
  ).argument("<sessionId>", "\u6C9F\u901A\u7FA4 ID").action(async (sessionId) => {
7007
7189
  await runPull(sessionId);
7008
7190
  });
7009
- program.command("dev-gate").description("\u67E5\u8BE2\u672C\u7AEF\u5F00\u53D1\u95E8\u7981\u72B6\u6001\uFF08\u5047\u8BBE\u786E\u8BA4 + \u9700\u6C42\u7406\u89E3\u786E\u8BA4\uFF09").argument("<sessionId>", "\u6C9F\u901A\u7FA4 ID").requiredOption(
7010
- "--source <source>",
7011
- "FRONTEND_PLAN \u6216 BACKEND_PLAN"
7012
- ).action(async (sessionId, opts) => {
7013
- const source = opts.source === "BACKEND_PLAN" ? AssumptionPlanSource.BACKEND_PLAN : AssumptionPlanSource.FRONTEND_PLAN;
7191
+ program.command("dev-gate").description("\u67E5\u8BE2\u672C\u7AEF\u5F00\u53D1\u95E8\u7981\u72B6\u6001\uFF08\u5047\u8BBE\u786E\u8BA4 + \u9700\u6C42\u7406\u89E3\u786E\u8BA4\uFF09").argument("<sessionId>", "\u6C9F\u901A\u7FA4 ID").requiredOption("--source <source>", "FRONTEND_PLAN \u6216 BACKEND_PLAN").action(async (sessionId, opts) => {
7192
+ const source = opts.source === "BACKEND_PLAN" ? "BACKEND_PLAN" /* BACKEND_PLAN */ : "FRONTEND_PLAN" /* FRONTEND_PLAN */;
7014
7193
  await runDevGate(sessionId, source);
7015
7194
  });
7016
7195
  program.command("assumptions").description("\uFF08\u5DF2\u5E9F\u5F03\uFF09\u8BF7\u4F7F\u7528 dev-gate").argument("<sessionId>", "\u6C9F\u901A\u7FA4 ID").action(async () => {
7017
- console.error("[apm] assumptions \u547D\u4EE4\u5DF2\u5E9F\u5F03\uFF0C\u8BF7\u4F7F\u7528: apm dev-gate <sessionId> --source FRONTEND_PLAN|BACKEND_PLAN");
7196
+ console.error(
7197
+ "[apm] assumptions \u547D\u4EE4\u5DF2\u5E9F\u5F03\uFF0C\u8BF7\u4F7F\u7528: apm dev-gate <sessionId> --source FRONTEND_PLAN|BACKEND_PLAN"
7198
+ );
7018
7199
  process.exit(1);
7019
7200
  });
7020
7201
  program.command("sync-document").description("\u5C06\u672C\u5730 Markdown \u8986\u76D6\u5F0F upsert \u5230\u5E73\u53F0\u4EFB\u52A1\u6587\u6863").argument("<sessionId>", "\u6C9F\u901A\u7FA4 ID").requiredOption(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-project-manage-cli",
3
- "version": "6.0.92",
3
+ "version": "6.0.94",
4
4
  "description": "命令行工具:后续用于调用平台后端 API 完成运维与自动化操作",
5
5
  "type": "module",
6
6
  "private": false,