@indigoai-us/hq-cli 5.45.0 → 5.45.1

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.
@@ -1,5 +1,5 @@
1
1
 
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="c72d8e20-42a6-5818-b5d3-985e53954430")}catch(e){}}();
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="8068c3b8-ee9c-5296-9b49-aa10bf38ccbd")}catch(e){}}();
3
3
  import chalk from "chalk";
4
4
  import open from "open";
5
5
  import { ensureCognitoToken } from "../utils/cognito-session.js";
@@ -178,7 +178,20 @@ export function registerFilesCommand(program) {
178
178
  console.error(chalk.red("Not authorized to modify this file prefix's ACL"));
179
179
  }
180
180
  else if (res.status === 404) {
181
- console.log(chalk.green(`Grant already absent for '${canonicalPrefix}' / ${principalLabel}`));
181
+ // A 404 means ZERO rows were removed — never report a green success
182
+ // that implies a revoke happened. Distinguish the two causes:
183
+ // ACL_NOT_FOUND — the prefix has no ACL at all (nothing was ever
184
+ // shared here): genuinely, benignly absent.
185
+ // GRANT_NOT_FOUND (or an older server with no code) — the ACL
186
+ // exists but no grant matched this principal:
187
+ // surface it as a warning so a typo'd/stale
188
+ // principal isn't mistaken for a real removal.
189
+ if (body.code === "ACL_NOT_FOUND") {
190
+ console.log(chalk.dim(`Nothing to remove — '${canonicalPrefix}' has no sharing grants.`));
191
+ }
192
+ else {
193
+ console.warn(chalk.yellow(`No matching grant for ${principalLabel} on '${canonicalPrefix}' — nothing was removed.`));
194
+ }
182
195
  return;
183
196
  }
184
197
  else if (res.status >= 500) {
@@ -460,4 +473,4 @@ async function runShareSession(params) {
460
473
  }
461
474
  }
462
475
  //# sourceMappingURL=files.js.map
463
- //# debugId=c72d8e20-42a6-5818-b5d3-985e53954430
476
+ //# debugId=8068c3b8-ee9c-5296-9b49-aa10bf38ccbd
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.45.0",
3
+ "version": "5.45.1",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -502,3 +502,91 @@ describe("hq files share — direct-grant fork (with --with)", () => {
502
502
  expect(fetchSpy).not.toHaveBeenCalled();
503
503
  });
504
504
  });
505
+
506
+ // ---------------------------------------------------------------------------
507
+ // hq files unshare — never reports a false "removed" on a zero-row revoke
508
+ // (Ridge / Connor #5, CLI side)
509
+ // ---------------------------------------------------------------------------
510
+
511
+ describe("hq files unshare", () => {
512
+ function mockCompanyResolution() {
513
+ // /membership/me for getCompanyUid
514
+ fetchSpy.mockResolvedValueOnce(
515
+ jsonResponse(200, {
516
+ memberships: [
517
+ { membershipKey: "k1", companyUid: "cmp_acme", role: "member", status: "active" },
518
+ ],
519
+ }),
520
+ );
521
+ }
522
+
523
+ it("prints a green success ONLY when the server actually removed a grant (200)", async () => {
524
+ mockCompanyResolution();
525
+ fetchSpy.mockResolvedValueOnce(jsonResponse(200, { acl: { path: "fooPath" } }));
526
+
527
+ const program = buildProgram();
528
+ await program.parseAsync(
529
+ ["files", "unshare", "fooPath", "--with", "user@example.com"],
530
+ { from: "user" },
531
+ );
532
+
533
+ const logs = logSpy.mock.calls.map((c) => String(c[0])).join("\n");
534
+ expect(logs).toMatch(/Removed grant for user@example.com on 'fooPath'/);
535
+ });
536
+
537
+ it("#5: does NOT print a green 'removed' success when the revoke matched zero rows (GRANT_NOT_FOUND)", async () => {
538
+ mockCompanyResolution();
539
+ fetchSpy.mockResolvedValueOnce(
540
+ jsonResponse(404, { error: "grant not found", code: "GRANT_NOT_FOUND" }),
541
+ );
542
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => undefined);
543
+
544
+ const program = buildProgram();
545
+ // Must NOT exit non-zero (idempotent no-op), and must NOT throw.
546
+ await program.parseAsync(
547
+ ["files", "unshare", "fooPath", "--with", "user@example.com"],
548
+ { from: "user" },
549
+ );
550
+
551
+ const logs = logSpy.mock.calls.map((c) => String(c[0])).join("\n");
552
+ const warns = warnSpy.mock.calls.map((c) => String(c[0])).join("\n");
553
+ // The dangerous false-success line must be absent.
554
+ expect(logs).not.toMatch(/Removed grant/);
555
+ // A truthful "nothing was removed" warning is surfaced instead.
556
+ expect(warns).toMatch(/nothing was removed/i);
557
+ });
558
+
559
+ it("treats a prefix with no ACL (ACL_NOT_FOUND) as benignly absent, not a removal", async () => {
560
+ mockCompanyResolution();
561
+ fetchSpy.mockResolvedValueOnce(
562
+ jsonResponse(404, { error: "ACL not found", code: "ACL_NOT_FOUND" }),
563
+ );
564
+
565
+ const program = buildProgram();
566
+ await program.parseAsync(
567
+ ["files", "unshare", "never-shared", "--with", "user@example.com"],
568
+ { from: "user" },
569
+ );
570
+
571
+ const logs = logSpy.mock.calls.map((c) => String(c[0])).join("\n");
572
+ expect(logs).not.toMatch(/Removed grant/);
573
+ expect(logs).toMatch(/has no sharing grants/);
574
+ });
575
+
576
+ it("falls back to a warning for an older server that returns 404 without a code", async () => {
577
+ mockCompanyResolution();
578
+ fetchSpy.mockResolvedValueOnce(jsonResponse(404, { error: "grant not found" }));
579
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => undefined);
580
+
581
+ const program = buildProgram();
582
+ await program.parseAsync(
583
+ ["files", "unshare", "fooPath", "--with", "user@example.com"],
584
+ { from: "user" },
585
+ );
586
+
587
+ const logs = logSpy.mock.calls.map((c) => String(c[0])).join("\n");
588
+ const warns = warnSpy.mock.calls.map((c) => String(c[0])).join("\n");
589
+ expect(logs).not.toMatch(/Removed grant/);
590
+ expect(warns).toMatch(/nothing was removed/i);
591
+ });
592
+ });
@@ -243,7 +243,25 @@ export function registerFilesCommand(program: Command): Command {
243
243
  } else if (res.status === 403) {
244
244
  console.error(chalk.red("Not authorized to modify this file prefix's ACL"));
245
245
  } else if (res.status === 404) {
246
- console.log(chalk.green(`Grant already absent for '${canonicalPrefix}' / ${principalLabel}`));
246
+ // A 404 means ZERO rows were removed — never report a green success
247
+ // that implies a revoke happened. Distinguish the two causes:
248
+ // ACL_NOT_FOUND — the prefix has no ACL at all (nothing was ever
249
+ // shared here): genuinely, benignly absent.
250
+ // GRANT_NOT_FOUND (or an older server with no code) — the ACL
251
+ // exists but no grant matched this principal:
252
+ // surface it as a warning so a typo'd/stale
253
+ // principal isn't mistaken for a real removal.
254
+ if (body.code === "ACL_NOT_FOUND") {
255
+ console.log(
256
+ chalk.dim(`Nothing to remove — '${canonicalPrefix}' has no sharing grants.`),
257
+ );
258
+ } else {
259
+ console.warn(
260
+ chalk.yellow(
261
+ `No matching grant for ${principalLabel} on '${canonicalPrefix}' — nothing was removed.`,
262
+ ),
263
+ );
264
+ }
247
265
  return;
248
266
  } else if (res.status >= 500) {
249
267
  console.error(chalk.red(`Server error: ${body.error ?? res.statusText}`));