(
opts: UpdateOptions,
deps: ProjectDeps = {},
)
| 237 | } |
| 238 | |
| 239 | export async function runUpdate( |
| 240 | opts: UpdateOptions, |
| 241 | deps: ProjectDeps = {}, |
| 242 | ): Promise<CliUpdateProjectResponse> { |
| 243 | const out = makeOutput(opts.output, deps); |
| 244 | const stderr = deps.stderr ?? ((line: string) => process.stderr.write(`${line}\n`)); |
| 245 | |
| 246 | // P1-2: validate idempotency key before sending as an HTTP header. |
| 247 | assertIdempotencyKey(opts.idempotencyKey); |
| 248 | |
| 249 | // P1-3: client-side length checks matching server limits. |
| 250 | if (opts.name !== undefined && opts.name.length > 200) { |
| 251 | throw localValidationError('--name must be at most 200 characters'); |
| 252 | } |
| 253 | if (opts.description !== undefined && opts.description.length > 2000) { |
| 254 | throw localValidationError('--description must be at most 2000 characters'); |
| 255 | } |
| 256 | |
| 257 | // Resolve password |
| 258 | let password = opts.password; |
| 259 | if (password === undefined && opts.passwordFile !== undefined) { |
| 260 | password = readFileSync(opts.passwordFile, 'utf8').trim(); |
| 261 | } |
| 262 | |
| 263 | // P2-7: guard --url against localhost/RFC1918/non-http(s). |
| 264 | if (opts.targetUrl !== undefined) { |
| 265 | assertNotLocal(opts.targetUrl); |
| 266 | } |
| 267 | |
| 268 | const mutableFields: Record<string, string | undefined> = { |
| 269 | name: opts.name, |
| 270 | targetUrl: opts.targetUrl, |
| 271 | username: opts.username, |
| 272 | password, |
| 273 | description: opts.description, |
| 274 | instruction: opts.instruction, |
| 275 | }; |
| 276 | const presentFields = Object.entries(mutableFields).filter(([, v]) => v !== undefined); |
| 277 | if (presentFields.length === 0) { |
| 278 | throw localValidationError( |
| 279 | 'At least one mutable flag is required: --name, --url, --username, --password/--password-file, --description, or --instruction.', |
| 280 | ); |
| 281 | } |
| 282 | |
| 283 | if (opts.dryRun) { |
| 284 | const idempotencyKey = opts.idempotencyKey ?? `cli-proj-update-${randomUUID()}`; |
| 285 | if ( |
| 286 | opts.idempotencyKey === undefined && |
| 287 | (opts.output === 'json' || opts.verbose || opts.debug) |
| 288 | ) { |
| 289 | stderr(`idempotency-key: ${idempotencyKey}`); |
| 290 | } |
| 291 | const sample: CliUpdateProjectResponse = { |
| 292 | id: opts.projectId, |
| 293 | updatedFields: presentFields.map(([k]) => k), |
| 294 | updatedAt: '2026-05-16T00:00:00.000Z', |
| 295 | }; |
| 296 | out.print(sample, data => renderUpdateText(data as CliUpdateProjectResponse)); |
no test coverage detected