Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix @handle URLs to use /@handle paths + add ac-news edit command

- renderHandle and autoLinkHandles now link to /@username (not /username)
- new `ac-news edit <code>` CLI command with:
--title, --body, --url, --editor, --replace/--with, --dry-run

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+116 -2
+114
at/news-cli.mjs
··· 11 11 * ac-news commits # show recent commits for reference 12 12 * ac-news commits --since "1 week ago" 13 13 * ac-news list # list recent posts 14 + * ac-news edit <code> --replace "old" --with "new" # find & replace in body 15 + * ac-news edit <code> --editor # edit body in $EDITOR 14 16 * ac-news delete <code> # delete a post (admin) 15 17 */ 16 18 ··· 299 301 }); 300 302 } 301 303 304 + async function commandEdit(args) { 305 + const code = args._[1]; 306 + if (!code) { 307 + console.error( 308 + 'Usage: ac-news edit <code> [options]\n' + 309 + ' ac-news edit ncd2 --title "New Title"\n' + 310 + ' ac-news edit ncd2 --body "New body text"\n' + 311 + ' ac-news edit ncd2 --editor # open $EDITOR with current body\n' + 312 + ' ac-news edit ncd2 --url "https://..."\n' + 313 + ' ac-news edit ncd2 --replace "old text" --with "new text"', 314 + ); 315 + process.exit(1); 316 + } 317 + 318 + if (!ADMIN_SUB) { 319 + console.error("ADMIN_SUB not set."); 320 + process.exit(1); 321 + } 322 + 323 + const dryRun = !!args["dry-run"]; 324 + 325 + await withDb(async (db) => { 326 + const posts = db.collection("news-posts"); 327 + const post = await posts.findOne({ code }); 328 + 329 + if (!post) { 330 + console.error(`Post not found: ${code}`); 331 + process.exit(1); 332 + } 333 + 334 + console.log(`\nEditing: "${post.title}" (${code})`); 335 + 336 + const updates = {}; 337 + 338 + // --title "New title" 339 + if (args.title) { 340 + updates.title = args.title; 341 + console.log(` title → "${args.title}"`); 342 + } 343 + 344 + // --url "https://..." 345 + if (args.url !== undefined) { 346 + updates.url = args.url; 347 + console.log(` url → "${args.url}"`); 348 + } 349 + 350 + // --replace "old" --with "new" (find-and-replace in body text) 351 + if (args.replace && args.with !== undefined) { 352 + const oldText = post.text || ""; 353 + const count = oldText.split(args.replace).length - 1; 354 + if (count === 0) { 355 + console.error(` Replace string not found in body: "${args.replace}"`); 356 + process.exit(1); 357 + } 358 + updates.text = oldText.replaceAll(args.replace, args.with); 359 + console.log(` body: replaced ${count} occurrence(s) of "${args.replace}" → "${args.with}"`); 360 + } 361 + 362 + // --body "Full new body" 363 + if (args.body) { 364 + updates.text = args.body; 365 + console.log(` body → ${args.body.length} chars`); 366 + } 367 + 368 + // --editor: open current body in $EDITOR 369 + if (args.editor) { 370 + const editor = process.env.EDITOR || "vi"; 371 + const tmpFile = join(tmpdir(), `ac-news-edit-${Date.now()}.md`); 372 + writeFileSync(tmpFile, post.text || ""); 373 + try { 374 + execSync(`${editor} ${tmpFile}`, { stdio: "inherit" }); 375 + const newBody = readFileSync(tmpFile, "utf8").trim(); 376 + if (newBody === (post.text || "").trim()) { 377 + console.log(" No changes made."); 378 + return; 379 + } 380 + updates.text = newBody; 381 + console.log(` body → ${newBody.length} chars (via editor)`); 382 + } finally { 383 + try { unlinkSync(tmpFile); } catch {} 384 + } 385 + } 386 + 387 + if (Object.keys(updates).length === 0) { 388 + console.log(" Nothing to update. Use --title, --body, --url, --replace, or --editor."); 389 + return; 390 + } 391 + 392 + updates.updated = new Date(); 393 + 394 + if (dryRun) { 395 + console.log("\n--dry-run: not saving."); 396 + if (updates.text) { 397 + console.log("\nNew body preview:\n"); 398 + console.log(updates.text); 399 + } 400 + return; 401 + } 402 + 403 + await posts.updateOne({ code }, { $set: updates }); 404 + console.log(`\nSaved: https://news.aesthetic.computer/${code}`); 405 + }); 406 + } 407 + 302 408 async function commandDelete(args) { 303 409 const code = args._[1]; 304 410 if (!code) { ··· 345 451 346 452 Manage: 347 453 list [--limit N] List recent posts 454 + edit <code> --title "New Title" Edit post title 455 + edit <code> --body "New body" Replace post body 456 + edit <code> --editor Edit body in $EDITOR 457 + edit <code> --url "https://..." Change post URL 458 + edit <code> --replace "old" --with "new" Find & replace in body 459 + edit ... --dry-run Preview without saving 348 460 delete <code> Delete a post (admin) 349 461 350 462 Examples: ··· 352 464 ac-news post "Dev Update" "The native OS build system got a major overhaul..." 353 465 ac-news post "Weekly Update" --file updates/2026-03-24.md 354 466 ac-news post "What's New" --editor 467 + ac-news edit ncd2 --replace "https://aesthetic.computer)" --with "https://aesthetic.computer/chat)" 355 468 ac-news list 356 469 `); 357 470 } ··· 364 477 commits: commandCommits, 365 478 post: commandPost, 366 479 list: commandList, 480 + edit: commandEdit, 367 481 delete: commandDelete, 368 482 }; 369 483
+2 -2
system/netlify/functions/news.mjs
··· 399 399 // Extract username without @ for the URL 400 400 const username = safeHandle.startsWith("@") ? safeHandle.slice(1) : safeHandle; 401 401 if (username === "anon") return safeHandle; 402 - const profileUrl = `https://aesthetic.computer/${username}`; 402 + const profileUrl = `https://aesthetic.computer/@${username}`; 403 403 return `<a href="${profileUrl}" class="news-modal-link news-handle-link" data-modal-url="${profileUrl}">${safeHandle}</a>`; 404 404 } 405 405 ··· 411 411 return html.replace(/(<[^>]*>)|(@([a-zA-Z0-9_-]+))/g, (match, tag, mention, username) => { 412 412 if (tag) return tag; // Pass through HTML tags unchanged. 413 413 if (username === "anon") return match; 414 - const profileUrl = `https://aesthetic.computer/${username}`; 414 + const profileUrl = `https://aesthetic.computer/@${username}`; 415 415 return `<a href="${profileUrl}" class="news-modal-link news-handle-link" data-modal-url="${profileUrl}">@${username}</a>`; 416 416 }); 417 417 }