agim-cli 1.0.8 → 1.0.10

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 (56) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/dist/cli.js +64 -0
  3. package/dist/cli.js.map +1 -1
  4. package/dist/core/admin-allowlist.d.ts +25 -0
  5. package/dist/core/admin-allowlist.d.ts.map +1 -0
  6. package/dist/core/admin-allowlist.js +130 -0
  7. package/dist/core/admin-allowlist.js.map +1 -0
  8. package/dist/core/admin-bootstrap.d.ts +28 -0
  9. package/dist/core/admin-bootstrap.d.ts.map +1 -0
  10. package/dist/core/admin-bootstrap.js +132 -0
  11. package/dist/core/admin-bootstrap.js.map +1 -0
  12. package/dist/core/approval-router.d.ts.map +1 -1
  13. package/dist/core/approval-router.js +15 -0
  14. package/dist/core/approval-router.js.map +1 -1
  15. package/dist/core/commands/builtin.d.ts +1 -1
  16. package/dist/core/commands/builtin.d.ts.map +1 -1
  17. package/dist/core/commands/builtin.js +8 -4
  18. package/dist/core/commands/builtin.js.map +1 -1
  19. package/dist/core/commands/service.d.ts +14 -0
  20. package/dist/core/commands/service.d.ts.map +1 -0
  21. package/dist/core/commands/service.js +85 -0
  22. package/dist/core/commands/service.js.map +1 -0
  23. package/dist/core/commands/setup.d.ts +3 -0
  24. package/dist/core/commands/setup.d.ts.map +1 -0
  25. package/dist/core/commands/setup.js +64 -0
  26. package/dist/core/commands/setup.js.map +1 -0
  27. package/dist/core/restart-completion.d.ts +5 -0
  28. package/dist/core/restart-completion.d.ts.map +1 -0
  29. package/dist/core/restart-completion.js +156 -0
  30. package/dist/core/restart-completion.js.map +1 -0
  31. package/dist/core/restart-flow.d.ts +40 -0
  32. package/dist/core/restart-flow.d.ts.map +1 -0
  33. package/dist/core/restart-flow.js +177 -0
  34. package/dist/core/restart-flow.js.map +1 -0
  35. package/dist/core/restart-preflight.d.ts +7 -0
  36. package/dist/core/restart-preflight.d.ts.map +1 -0
  37. package/dist/core/restart-preflight.js +95 -0
  38. package/dist/core/restart-preflight.js.map +1 -0
  39. package/dist/core/router.d.ts.map +1 -1
  40. package/dist/core/router.js +23 -2
  41. package/dist/core/router.js.map +1 -1
  42. package/dist/core/self-protect.d.ts +8 -0
  43. package/dist/core/self-protect.d.ts.map +1 -0
  44. package/dist/core/self-protect.js +119 -0
  45. package/dist/core/self-protect.js.map +1 -0
  46. package/dist/core/service-intent.d.ts +17 -0
  47. package/dist/core/service-intent.d.ts.map +1 -0
  48. package/dist/core/service-intent.js +87 -0
  49. package/dist/core/service-intent.js.map +1 -0
  50. package/dist/core/types.d.ts +8 -1
  51. package/dist/core/types.d.ts.map +1 -1
  52. package/dist/web/public/settings.html +133 -0
  53. package/dist/web/server.d.ts.map +1 -1
  54. package/dist/web/server.js +110 -37
  55. package/dist/web/server.js.map +1 -1
  56. package/package.json +1 -1
@@ -431,6 +431,19 @@ export async function startWebServer(options) {
431
431
  if (url.pathname === '/api/service/restart' && req.method === 'POST') {
432
432
  return handleServiceRestart(res);
433
433
  }
434
+ // Admin allowlist — list + add/remove via the Safety card. Locally
435
+ // gated: only allowed when bound to 127.0.0.1, since the page has
436
+ // no auth and we don't want random LAN visitors granting themselves
437
+ // admin. See `bindHost` upthread.
438
+ if (url.pathname === '/api/admin-allowlist' && req.method === 'GET') {
439
+ return handleAdminAllowlistGet(res);
440
+ }
441
+ if (url.pathname === '/api/admin-allowlist' && req.method === 'POST') {
442
+ return handleAdminAllowlistAdd(req, res, bindHost);
443
+ }
444
+ if (url.pathname === '/api/admin-allowlist' && req.method === 'DELETE') {
445
+ return handleAdminAllowlistRemove(req, res, bindHost);
446
+ }
434
447
  res.writeHead(404);
435
448
  res.end('Not found');
436
449
  });
@@ -1039,7 +1052,7 @@ async function handleServiceStop(res) {
1039
1052
  }, 200);
1040
1053
  }
1041
1054
  async function handleServiceRestart(res) {
1042
- const { detectService, PID_FILE, LOG_FILE } = await import('../cli-ui/service.js');
1055
+ const { detectService } = await import('../cli-ui/service.js');
1043
1056
  const st = detectService();
1044
1057
  if (st.mode === 'systemd') {
1045
1058
  try {
@@ -1060,44 +1073,104 @@ async function handleServiceRestart(res) {
1060
1073
  sendJson(res, 409, { error: 'foreground service is running in another terminal; restart it manually (Ctrl-C + re-run)' });
1061
1074
  return;
1062
1075
  }
1063
- // background or 'none': spawn a detached node -e helper that waits
1064
- // for our PID to die, then starts a fresh daemon. Then SIGTERM self
1065
- // so the helper's wait-loop unblocks and the new daemon takes over.
1066
- const __dn = dirname(fileURLToPath(import.meta.url));
1067
- const cliJs = join(__dn, '..', 'cli.js');
1068
- const parentPid = process.pid;
1069
- const helperCode = 'const{spawn}=require("child_process");' +
1070
- 'const fs=require("fs");' +
1071
- `const pid=${parentPid};` +
1072
- 'let n=0;' +
1073
- 'const t=setInterval(()=>{' +
1074
- ' try{process.kill(pid,0)}catch{' +
1075
- ' clearInterval(t);' +
1076
- ` const fd=fs.openSync(${JSON.stringify(LOG_FILE)},"a");` +
1077
- ` const c=spawn(${JSON.stringify(process.execPath)},[${JSON.stringify(cliJs)},"start"],{detached:true,stdio:["ignore",fd,fd],env:process.env});` +
1078
- ' c.unref();' +
1079
- ` fs.writeFileSync(${JSON.stringify(PID_FILE)},String(c.pid)+"\\n");` +
1080
- ' fs.closeSync(fd);' +
1081
- ' process.exit(0);' +
1082
- ' }' +
1083
- ' if(++n>150){clearInterval(t);process.exit(1)}' +
1084
- '},200);';
1085
- const { spawn: spawnChild } = await import('node:child_process');
1086
- const helper = spawnChild(process.execPath, ['-e', helperCode], {
1087
- detached: true,
1088
- stdio: 'ignore',
1089
- env: process.env,
1090
- });
1091
- helper.unref();
1092
- sendJson(res, 200, { ok: true, mode: st.mode, restarting: true });
1093
- setTimeout(() => {
1094
- try {
1095
- process.kill(process.pid, 'SIGTERM');
1076
+ // background or 'none': delegate to the shared restart-flow. The flow
1077
+ // runs pre-flight checks, writes restart-pending.json with source='web'
1078
+ // (so the new daemon doesn't try to push an IM completion message
1079
+ // browser polling handles that), and spawns the detached helper.
1080
+ const { initiateRestart } = await import('../core/restart-flow.js');
1081
+ const result = await initiateRestart({ source: 'web' });
1082
+ if (!result.ok) {
1083
+ sendJson(res, 409, { error: result.message, details: result.errors });
1084
+ return;
1085
+ }
1086
+ sendJson(res, 200, { ok: true, mode: st.mode, restarting: true, message: result.message });
1087
+ }
1088
+ // ============================================================
1089
+ // Admin allowlist endpoints (Safety card → Admin Users)
1090
+ // ============================================================
1091
+ //
1092
+ // All three are gated on the server's bindHost being a loopback address.
1093
+ // The web console has no auth, so we MUST not let a LAN visitor add
1094
+ // themselves as admin. When bindHost is 0.0.0.0 we hide the editor on
1095
+ // the frontend AND refuse here as a defense-in-depth.
1096
+ function isLocalBind(bindHost) {
1097
+ return bindHost === '127.0.0.1' || bindHost === '::1' || bindHost === 'localhost';
1098
+ }
1099
+ async function handleAdminAllowlistGet(res) {
1100
+ try {
1101
+ const { listAdmins, isAllowlistConfigured } = await import('../core/admin-allowlist.js');
1102
+ const { BOOTSTRAP_TOKEN_FILE } = await import('../core/admin-bootstrap.js');
1103
+ const hasBootstrap = existsSync(BOOTSTRAP_TOKEN_FILE);
1104
+ sendJson(res, 200, {
1105
+ admins: listAdmins(),
1106
+ configured: isAllowlistConfigured(),
1107
+ bootstrapAvailable: hasBootstrap,
1108
+ bootstrapTokenPath: BOOTSTRAP_TOKEN_FILE,
1109
+ });
1110
+ }
1111
+ catch (err) {
1112
+ sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
1113
+ }
1114
+ }
1115
+ async function handleAdminAllowlistAdd(req, res, bindHost) {
1116
+ if (!isLocalBind(bindHost)) {
1117
+ sendJson(res, 403, {
1118
+ error: 'admin editor disabled when web is bound publicly (no auth on this endpoint).',
1119
+ });
1120
+ return;
1121
+ }
1122
+ try {
1123
+ const body = await readBody(req, res);
1124
+ const parsed = JSON.parse(body || '{}');
1125
+ const platform = (parsed.platform || '').trim();
1126
+ const userId = (parsed.userId || '').trim();
1127
+ if (!platform || !userId) {
1128
+ sendJson(res, 400, { error: 'platform and userId required' });
1129
+ return;
1096
1130
  }
1097
- catch {
1098
- process.exit(0);
1131
+ const { promoteAdmin, listAdmins } = await import('../core/admin-allowlist.js');
1132
+ const added = await promoteAdmin(platform, userId);
1133
+ sendJson(res, 200, { ok: true, added, admins: listAdmins() });
1134
+ }
1135
+ catch (err) {
1136
+ sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
1137
+ }
1138
+ }
1139
+ async function handleAdminAllowlistRemove(req, res, bindHost) {
1140
+ if (!isLocalBind(bindHost)) {
1141
+ sendJson(res, 403, { error: 'admin editor disabled on public binds' });
1142
+ return;
1143
+ }
1144
+ try {
1145
+ const body = await readBody(req, res);
1146
+ const parsed = JSON.parse(body || '{}');
1147
+ const platform = (parsed.platform || '').trim().toLowerCase();
1148
+ const userId = (parsed.userId || '').trim();
1149
+ if (!platform || !userId) {
1150
+ sendJson(res, 400, { error: 'platform and userId required' });
1151
+ return;
1099
1152
  }
1100
- }, 300);
1153
+ const { listAdmins } = await import('../core/admin-allowlist.js');
1154
+ const remaining = listAdmins().filter((a) => !(a.platform === platform && a.userId === userId));
1155
+ const newRaw = remaining.map((a) => `${a.platform}:${a.userId}`).join(',');
1156
+ // Persist to env file. Note this nukes both runtime + env-derived
1157
+ // entries — fine for the editor's use case (it round-trips through
1158
+ // the full list).
1159
+ const { updateEnvFile } = await import('../cli-ui/env-file.js');
1160
+ updateEnvFile({ IMHUB_ADMIN_USERS: newRaw || null });
1161
+ // Update process.env so getEntries() picks it up next call.
1162
+ if (newRaw)
1163
+ process.env.IMHUB_ADMIN_USERS = newRaw;
1164
+ else
1165
+ delete process.env.IMHUB_ADMIN_USERS;
1166
+ // Reset the parse cache so the change is visible immediately.
1167
+ const { _resetCache } = await import('../core/admin-allowlist.js');
1168
+ _resetCache();
1169
+ sendJson(res, 200, { ok: true, admins: remaining });
1170
+ }
1171
+ catch (err) {
1172
+ sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
1173
+ }
1101
1174
  }
1102
1175
  /**
1103
1176
  * POST /api/notify → push a message to an IM thread.