chainlesschain 0.162.13 → 0.162.14

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 (145) hide show
  1. package/README.md +1 -1
  2. package/package.json +2 -2
  3. package/src/assets/web-panel/assets/{AIOps-Bq_zxhCr.js → AIOps-D34d_Nh1.js} +1 -1
  4. package/src/assets/web-panel/assets/{ActionButton-CaevDm9t.js → ActionButton-Br7HxCnl.js} +1 -1
  5. package/src/assets/web-panel/assets/{Analytics-B0gOmwPw.js → Analytics-bVKq79Xd.js} +1 -1
  6. package/src/assets/web-panel/assets/{AppLayout-DWhZiV0Q.js → AppLayout-CWSLIbAz.js} +2 -2
  7. package/src/assets/web-panel/assets/{Audit-ZuZJBCxo.js → Audit-Cmnu1qqa.js} +1 -1
  8. package/src/assets/web-panel/assets/{Backup-CX7jhH5l.js → Backup-Rok20-TL.js} +1 -1
  9. package/src/assets/web-panel/assets/{BaseInput-CVLx7HVq.js → BaseInput-BJzs_ZtT.js} +1 -1
  10. package/src/assets/web-panel/assets/{Chat-C6yL5tRD.js → Chat-CSYapbcq.js} +1 -1
  11. package/src/assets/web-panel/assets/{Checkbox-BePhbqVq.js → Checkbox-BEa7Sr7e.js} +1 -1
  12. package/src/assets/web-panel/assets/{Codegen-B5E4x1Lm.js → Codegen-C9M4e7ne.js} +1 -1
  13. package/src/assets/web-panel/assets/{Col-CWhNU6A7.js → Col-DU9NoUIi.js} +1 -1
  14. package/src/assets/web-panel/assets/{Community-mSEAuJhp.js → Community-DA9uz_jP.js} +1 -1
  15. package/src/assets/web-panel/assets/{Compact-DSudHzX3.js → Compact-3_bEraVw.js} +1 -1
  16. package/src/assets/web-panel/assets/{Compliance-CxJLYjyn.js → Compliance-BtF8jWUQ.js} +1 -1
  17. package/src/assets/web-panel/assets/{Cowork-C-trppQj.js → Cowork-BqvA7oaM.js} +1 -1
  18. package/src/assets/web-panel/assets/{Cron-_Ij4v5fY.js → Cron-CxZy7Mzg.js} +1 -1
  19. package/src/assets/web-panel/assets/{Crosschain-eLHXzp5T.js → Crosschain-1DB-XRGu.js} +1 -1
  20. package/src/assets/web-panel/assets/{DID-BP04AUUB.js → DID-B6Ezp1pt.js} +1 -1
  21. package/src/assets/web-panel/assets/{Dashboard-CzEeQv62.js → Dashboard-QDJ6VVsn.js} +2 -2
  22. package/src/assets/web-panel/assets/{Dropdown-C_SGOB22.js → Dropdown-CovsWjxG.js} +1 -1
  23. package/src/assets/web-panel/assets/{Federation-BgaP4BOv.js → Federation-DbRxS4Y4.js} +1 -1
  24. package/src/assets/web-panel/assets/{FormItemContext-BxGLLt9r.js → FormItemContext-9E9dNGtx.js} +1 -1
  25. package/src/assets/web-panel/assets/{Git-Bt_uM_Gw.js → Git-CqEpyxRZ.js} +1 -1
  26. package/src/assets/web-panel/assets/{Governance-N2-0RG_o.js → Governance-On47KtGq.js} +1 -1
  27. package/src/assets/web-panel/assets/{Inference-eS3g-CzP.js → Inference-RZcjcyaq.js} +1 -1
  28. package/src/assets/web-panel/assets/{KnowledgeGraph-CUuvNVah.js → KnowledgeGraph-C-1rRAM9.js} +1 -1
  29. package/src/assets/web-panel/assets/{Logs-wPbwEt2r.js → Logs-BuunmG_r.js} +1 -1
  30. package/src/assets/web-panel/assets/{Marketplace-DH91kTwo.js → Marketplace-CromymyA.js} +1 -1
  31. package/src/assets/web-panel/assets/{McpTools-D-GyyBrI.js → McpTools-5XlFExh7.js} +1 -1
  32. package/src/assets/web-panel/assets/{Memory-BOtUy-tw.js → Memory-DjnUT7YM.js} +1 -1
  33. package/src/assets/web-panel/assets/{MobileBridge-DIP__XQd.js → MobileBridge-BrYIgLg6.js} +1 -1
  34. package/src/assets/web-panel/assets/{MobileProjects-1nqr1UsU.js → MobileProjects-CL5V3fTm.js} +1 -1
  35. package/src/assets/web-panel/assets/{Mtc-CCE0x7h2.js → Mtc-CHYJq6zK.js} +1 -1
  36. package/src/assets/web-panel/assets/{MtcAudit-BBkz0XUO.js → MtcAudit-BZxUO0qt.js} +1 -1
  37. package/src/assets/web-panel/assets/{Multisig-CIKSJvTY.js → Multisig-FZTmJgW1.js} +1 -1
  38. package/src/assets/web-panel/assets/{NLProgramming-BDkgeFcq.js → NLProgramming-C9Mhefph.js} +1 -1
  39. package/src/assets/web-panel/assets/{Notes-ONiUxfN1.js → Notes-W7usj-Ar.js} +1 -1
  40. package/src/assets/web-panel/assets/{NotificationSettings-CGcyKEIe.js → NotificationSettings-PBuYv_Bh.js} +1 -1
  41. package/src/assets/web-panel/assets/{Organization-BZtMYBJt.js → Organization-CuYCE-rF.js} +1 -1
  42. package/src/assets/web-panel/assets/{Overflow-C4LfFZAI.js → Overflow-Dojx-kzE.js} +1 -1
  43. package/src/assets/web-panel/assets/{P2P-D71Cpk-m.js → P2P-BgIaSrLX.js} +1 -1
  44. package/src/assets/web-panel/assets/{Permissions-DRGYF29v.js → Permissions-Byj2dkF_.js} +1 -1
  45. package/src/assets/web-panel/assets/{PersonalDataHub-CfRYoIua.js → PersonalDataHub-CMOOI13-.js} +1 -1
  46. package/src/assets/web-panel/assets/{Pipeline-BOyp0_Qo.js → Pipeline-CWwEOF09.js} +1 -1
  47. package/src/assets/web-panel/assets/{Privacy-a_AcphvF.js → Privacy-VT7gldcN.js} +1 -1
  48. package/src/assets/web-panel/assets/{ProjectInit-CFu1grYt.js → ProjectInit-7UH3c3p7.js} +1 -1
  49. package/src/assets/web-panel/assets/{ProjectSettings-p54Eivhh.js → ProjectSettings-DqLp-72a.js} +1 -1
  50. package/src/assets/web-panel/assets/{Projects-DkB88XAu.js → Projects-B_54eDhH.js} +1 -1
  51. package/src/assets/web-panel/assets/{Providers-EK7f8DEd.js → Providers-BIrNfNpc.js} +1 -1
  52. package/src/assets/web-panel/assets/{QuickAsk-CpO0w3iP.js → QuickAsk-BbYPwCso.js} +1 -1
  53. package/src/assets/web-panel/assets/{Recommend-BUJVQdv9.js → Recommend-BF4qBssF.js} +1 -1
  54. package/src/assets/web-panel/assets/{Reputation-kU2fOuZt.js → Reputation-DPEzlC2V.js} +1 -1
  55. package/src/assets/web-panel/assets/{Row-oGWRbk6g.js → Row-DjHxhH1L.js} +1 -1
  56. package/src/assets/web-panel/assets/{RssFeed-saC_46Yo.js → RssFeed-D0_j678P.js} +1 -1
  57. package/src/assets/web-panel/assets/{Search-CjtRqFUu.js → Search-DctfGehu.js} +1 -1
  58. package/src/assets/web-panel/assets/{Security-BX0NBVfQ.js → Security-BFHggeYM.js} +1 -1
  59. package/src/assets/web-panel/assets/{Services-Bgxsnei_.js → Services-CmrFMukV.js} +1 -1
  60. package/src/assets/web-panel/assets/{Skeleton-CwBb3k2a.js → Skeleton-DR4vn_nS.js} +1 -1
  61. package/src/assets/web-panel/assets/{Skills-CZCMYH6Z.js → Skills-DlXG2yyV.js} +1 -1
  62. package/src/assets/web-panel/assets/{Sla-Djy1uHnZ.js → Sla-4PPGL3SE.js} +1 -1
  63. package/src/assets/web-panel/assets/{SpeechSettings-CGUI_Uyh.js → SpeechSettings-D9EhJOqm.js} +1 -1
  64. package/src/assets/web-panel/assets/{SyncSettings-DK2CKHRD.js → SyncSettings-Dasmbi0p.js} +1 -1
  65. package/src/assets/web-panel/assets/{Tasks-BKiOzeIO.js → Tasks-vilEiuPA.js} +1 -1
  66. package/src/assets/web-panel/assets/{Templates-CnQpleXj.js → Templates-Ca9Rvktn.js} +1 -1
  67. package/src/assets/web-panel/assets/{Tenant-DwKz0cjm.js → Tenant-CEZb9gfK.js} +1 -1
  68. package/src/assets/web-panel/assets/{Terminal-A7t_wsR8.js → Terminal-DanCBdbD.js} +1 -1
  69. package/src/assets/web-panel/assets/{Tokens-BqYY9l44.js → Tokens-SPkClW2d.js} +1 -1
  70. package/src/assets/web-panel/assets/{Trigger-BI4bXFmi.js → Trigger-B645yL7g.js} +1 -1
  71. package/src/assets/web-panel/assets/{Trust-yMynKTRG.js → Trust-D9sM_Ig0.js} +1 -1
  72. package/src/assets/web-panel/assets/{UkeySign-Br4IScM6.js → UkeySign-B_Nr2K-u.js} +1 -1
  73. package/src/assets/web-panel/assets/{VideoEditing-CWcThGsP.js → VideoEditing-U01Lea8j.js} +1 -1
  74. package/src/assets/web-panel/assets/{Wallet-CZcAtjxj.js → Wallet-6xBySVV8.js} +1 -1
  75. package/src/assets/web-panel/assets/{WebAuthn-BnTZFMA0.js → WebAuthn-DbgMoBu6.js} +1 -1
  76. package/src/assets/web-panel/assets/{WorkflowEditor-N7gGz3_n.js → WorkflowEditor-Bz-Y6IR2.js} +1 -1
  77. package/src/assets/web-panel/assets/{chat-D175ZIO0.js → chat-BC_O9hag.js} +1 -1
  78. package/src/assets/web-panel/assets/{colors-LKhZyttv.js → colors-ChlOGOvr.js} +1 -1
  79. package/src/assets/web-panel/assets/{compact-item-CsJSebxT.js → compact-item-BSbAYGGF.js} +1 -1
  80. package/src/assets/web-panel/assets/{createContext-BJ_CPYFC.js → createContext-CFcZly5M.js} +1 -1
  81. package/src/assets/web-panel/assets/{hasIn-CzD3IqH8.js → hasIn-BomYwwYE.js} +1 -1
  82. package/src/assets/web-panel/assets/{index-BSNibAqz.js → index-5Ewm6KZA.js} +1 -1
  83. package/src/assets/web-panel/assets/{index-D8kltMTW.js → index-B6LJHQoE.js} +1 -1
  84. package/src/assets/web-panel/assets/{index-BvF2tC6C.js → index-BEJ6YiLI.js} +1 -1
  85. package/src/assets/web-panel/assets/{index-BjOrt4vw.js → index-BGUbtM3R.js} +1 -1
  86. package/src/assets/web-panel/assets/{index-Dm-3kvtD.js → index-BHGsFwYW.js} +1 -1
  87. package/src/assets/web-panel/assets/{index-D401L3yx.js → index-BHi69MHF.js} +1 -1
  88. package/src/assets/web-panel/assets/{index-R1cFADfk.js → index-BI1jAWcc.js} +1 -1
  89. package/src/assets/web-panel/assets/index-BIRYt1of.js +1 -0
  90. package/src/assets/web-panel/assets/{index-Dd7dICwB.js → index-BYDvb1pi.js} +1 -1
  91. package/src/assets/web-panel/assets/{index-Kn-Of5ew.js → index-BZGdjNLA.js} +1 -1
  92. package/src/assets/web-panel/assets/{index-uTEVWPYA.js → index-BwFykZ5U.js} +1 -1
  93. package/src/assets/web-panel/assets/{index-JTX9A7w0.js → index-ByZQNO0A.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-BmJdof_c.js → index-C0rr1X9W.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-BsirlkJ0.js → index-CBhoZhCO.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-6qPbrYF7.js → index-CCRSz2cR.js} +1 -1
  97. package/src/assets/web-panel/assets/index-CZfySmWX.js +1 -0
  98. package/src/assets/web-panel/assets/{index-DJVkBmSc.js → index-Cj47XwJQ.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-DOO73rHE.js → index-Cmzh8gKL.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-BNVLVzN5.js → index-CnxlKTDK.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-CTIkCKav.js → index-CoF95pYK.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-RIO4JKMP.js → index-Ctx97mH-.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-BEDFHKO3.js → index-D0vX9jQA.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-CRGNuUIM.js → index-DNkth8dM.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-DnPt5OdD.js → index-DRp5_Xns.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-E7t1hAnk.js → index-DW-Ji07y.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-BhiZDGg7.js → index-DXgE2VW6.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-B7UYymse.js → index-D_0B3CiU.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-CyHdYUeZ.js → index-Dbf5YmDX.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-CKjBAdm0.js → index-DsNQ2hqI.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-C2HBKw07.js → index-EY733h9z.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-DM3uBEWD.js → index-QD_n54XT.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-DXp1jVsK.js → index-T5Y_9IPv.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-BDSZDDb2.js → index-b8GbH2Yi.js} +3 -3
  115. package/src/assets/web-panel/assets/{index-BZluCuTH.js → index-gUACAWbM.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-D7ZcBI5s.js → index-onW325hZ.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-D5IZCkZG.js → index-ozVPr1gj.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-BMvdoiFr.js → index-slYX2rCE.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-BXH9ujMW.js → index-t9u2bHpH.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-BV-__mlC.js → index-za1GUJBG.js} +1 -1
  121. package/src/assets/web-panel/assets/{initDefaultProps-CBW0okek.js → initDefaultProps-DnadEaxu.js} +1 -1
  122. package/src/assets/web-panel/assets/{motion-DGAffQ0Z.js → motion-CC_Na0Tl.js} +1 -1
  123. package/src/assets/web-panel/assets/{move-DFJ0-5IW.js → move-C2d9Mkk9.js} +1 -1
  124. package/src/assets/web-panel/assets/{omit-AvrDghg1.js → omit-QvpKbF8p.js} +1 -1
  125. package/src/assets/web-panel/assets/{pickAttrs-D7csw9i1.js → pickAttrs-Dm8r3X1_.js} +1 -1
  126. package/src/assets/web-panel/assets/{placementArrow-hZ6Lg6kG.js → placementArrow-DaqaVfoX.js} +1 -1
  127. package/src/assets/web-panel/assets/{responsiveObserve-DLLx5VvS.js → responsiveObserve-Iida9fIn.js} +1 -1
  128. package/src/assets/web-panel/assets/{slide-BaRIT3ev.js → slide-YqHexXQD.js} +1 -1
  129. package/src/assets/web-panel/assets/{statusUtils-Cdjyuhrz.js → statusUtils-BGKLoeEt.js} +1 -1
  130. package/src/assets/web-panel/assets/{styleChecker-CbrNybTt.js → styleChecker-aI-gsQO8.js} +1 -1
  131. package/src/assets/web-panel/assets/{useFlexGapSupport-B8gAhiRC.js → useFlexGapSupport-BiOsz4rc.js} +1 -1
  132. package/src/assets/web-panel/assets/{useFs-BZPy4ICP.js → useFs-CZy7Zo2X.js} +1 -1
  133. package/src/assets/web-panel/assets/{vnode-6Y0NDMVv.js → vnode-B6WqjmE4.js} +1 -1
  134. package/src/assets/web-panel/assets/{zoom-DTbMGsSH.js → zoom-DTeTrJ2z.js} +1 -1
  135. package/src/assets/web-panel/index.html +1 -1
  136. package/src/commands/__tests__/android.test.js +260 -0
  137. package/src/commands/android.js +284 -0
  138. package/src/commands/hub.js +57 -17
  139. package/src/gateways/ws/personal-data-hub-protocol.js +28 -8
  140. package/src/index.js +2 -0
  141. package/src/lib/cc-android-bridge.js +162 -0
  142. package/src/lib/personal-data-hub-wiring.js +74 -18
  143. package/src/lib/web-ui-server.js +2 -1
  144. package/src/assets/web-panel/assets/index-CY1mQA2I.js +0 -1
  145. package/src/assets/web-panel/assets/index-D9nXHfUB.js +0 -1
@@ -0,0 +1,260 @@
1
+ /**
2
+ * `cc android` command + cc-android-bridge unit tests (Plan A A7 scaffold).
3
+ *
4
+ * These exercise the command routing without a real Android device. The
5
+ * `_deps` injection seam swaps the bridge module with a mock so we hit both
6
+ * the success path (bridge resolves) and error path (bridge throws
7
+ * AndroidBridgeUnavailableError, which on a real non-Android host is what
8
+ * actually happens).
9
+ */
10
+
11
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
12
+
13
+ import * as androidCmd from "../android.js";
14
+ import * as bridge from "../../lib/cc-android-bridge.js";
15
+
16
+ let logSpy, errSpy, exitSpy, jsonSpy;
17
+
18
+ function withMockBridge(invokeResult) {
19
+ const mock = {
20
+ invoke: vi.fn(async (method, params) => {
21
+ if (invokeResult instanceof Error) throw invokeResult;
22
+ if (typeof invokeResult === "function")
23
+ return await invokeResult(method, params);
24
+ return invokeResult;
25
+ }),
26
+ caps: vi.fn(() => ({ available: true, reason: "mock" })),
27
+ AndroidBridgeUnavailableError: bridge.AndroidBridgeUnavailableError,
28
+ };
29
+ androidCmd._deps.bridge = mock;
30
+ return mock;
31
+ }
32
+
33
+ beforeEach(() => {
34
+ logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
35
+ errSpy = vi.spyOn(console, "error").mockImplementation(() => {});
36
+ exitSpy = vi.spyOn(process, "exit").mockImplementation((code) => {
37
+ throw new Error(`__exit:${code}`);
38
+ });
39
+ });
40
+
41
+ afterEach(() => {
42
+ androidCmd._deps.bridge = bridge;
43
+ vi.restoreAllMocks();
44
+ });
45
+
46
+ // ─── cc-android-bridge ────────────────────────────────────────────────
47
+
48
+ describe("cc-android-bridge", () => {
49
+ it("detectAndroid returns false on a vanilla non-Android host", () => {
50
+ // Test runs on win32 / linux / darwin — not android.
51
+ expect(bridge.detectAndroid()).toBe(false);
52
+ });
53
+
54
+ it("detectAndroid returns true under CC_ANDROID_BRIDGE_OVERRIDE=1", () => {
55
+ process.env.CC_ANDROID_BRIDGE_OVERRIDE = "1";
56
+ try {
57
+ expect(bridge.detectAndroid()).toBe(true);
58
+ } finally {
59
+ delete process.env.CC_ANDROID_BRIDGE_OVERRIDE;
60
+ }
61
+ });
62
+
63
+ it("invoke rejects with ANDROID_BRIDGE_NOT_AVAILABLE on non-Android host", async () => {
64
+ await expect(bridge.invoke("contacts.query", {})).rejects.toMatchObject({
65
+ code: "ANDROID_BRIDGE_NOT_AVAILABLE",
66
+ });
67
+ });
68
+
69
+ it("invoke routes through testInvoke when override is set", async () => {
70
+ process.env.CC_ANDROID_BRIDGE_OVERRIDE = "1";
71
+ const original = bridge._deps.testInvoke;
72
+ bridge._deps.testInvoke = vi.fn(async (m, p) => ({ echo: m, params: p }));
73
+ try {
74
+ const r = await bridge.invoke("foo.bar", { x: 1 });
75
+ expect(r).toEqual({ echo: "foo.bar", params: { x: 1 } });
76
+ expect(bridge._deps.testInvoke).toHaveBeenCalledWith("foo.bar", { x: 1 });
77
+ } finally {
78
+ bridge._deps.testInvoke = original;
79
+ delete process.env.CC_ANDROID_BRIDGE_OVERRIDE;
80
+ }
81
+ });
82
+
83
+ it("invoke rejects empty method", async () => {
84
+ await expect(bridge.invoke("")).rejects.toThrow(/non-empty string/);
85
+ });
86
+
87
+ it("caps reports unavailable on non-Android host", () => {
88
+ const c = bridge.caps();
89
+ expect(c.available).toBe(false);
90
+ expect(c.reason).toMatch(/not-on-android/);
91
+ });
92
+ });
93
+
94
+ // ─── cmd routing — bridge success path ─────────────────────────────────
95
+
96
+ describe("cc android commands — success path (mocked bridge)", () => {
97
+ it("cmdCaps prints available when bridge.caps returns available", async () => {
98
+ withMockBridge({});
99
+ await androidCmd._cmds.cmdCaps({ json: true });
100
+ expect(logSpy).toHaveBeenCalled();
101
+ const out = logSpy.mock.calls[0][0];
102
+ expect(JSON.parse(out).available).toBe(true);
103
+ });
104
+
105
+ it("cmdContactsPull invokes contacts.query with --since", async () => {
106
+ const mock = withMockBridge({ ingested: 42, events: [] });
107
+ await androidCmd._cmds.cmdContactsPull({
108
+ json: true,
109
+ since: "1700000000000",
110
+ });
111
+ expect(mock.invoke).toHaveBeenCalledWith("contacts.query", {
112
+ since: 1700000000000,
113
+ });
114
+ const out = JSON.parse(logSpy.mock.calls[0][0]);
115
+ expect(out.ingested).toBe(42);
116
+ });
117
+
118
+ it("cmdSmsPull invokes sms.query", async () => {
119
+ const mock = withMockBridge({ ok: true });
120
+ await androidCmd._cmds.cmdSmsPull({ json: true });
121
+ expect(mock.invoke).toHaveBeenCalledWith("sms.query", { since: undefined });
122
+ });
123
+
124
+ it("cmdCallsPull invokes calls.query", async () => {
125
+ const mock = withMockBridge({ ok: true });
126
+ await androidCmd._cmds.cmdCallsPull({ json: true });
127
+ expect(mock.invoke).toHaveBeenCalledWith("calls.query", {
128
+ since: undefined,
129
+ });
130
+ });
131
+
132
+ it("cmdAppList passes includeSystem flag", async () => {
133
+ const mock = withMockBridge([{ pkg: "com.foo" }]);
134
+ await androidCmd._cmds.cmdAppList({ json: true, system: true });
135
+ expect(mock.invoke).toHaveBeenCalledWith("app.list", {
136
+ includeSystem: true,
137
+ });
138
+ });
139
+
140
+ it("cmdAppLaunch forwards pkg", async () => {
141
+ const mock = withMockBridge({ ok: true });
142
+ await androidCmd._cmds.cmdAppLaunch("com.tencent.mm", { json: true });
143
+ expect(mock.invoke).toHaveBeenCalledWith("app.launch", {
144
+ pkg: "com.tencent.mm",
145
+ });
146
+ });
147
+
148
+ it("cmdAppIntent parses --extra K=V repeatable", async () => {
149
+ const mock = withMockBridge({ ok: true });
150
+ await androidCmd._cmds.cmdAppIntent("com.foo", "VIEW", {
151
+ json: true,
152
+ extra: ["url=https://x", "id=42"],
153
+ });
154
+ expect(mock.invoke).toHaveBeenCalledWith("app.intent", {
155
+ pkg: "com.foo",
156
+ action: "VIEW",
157
+ extras: { url: "https://x", id: "42" },
158
+ });
159
+ });
160
+
161
+ it("cmdFsRead forwards SAF tree URI", async () => {
162
+ const mock = withMockBridge({ bytes: 100 });
163
+ await androidCmd._cmds.cmdFsRead(
164
+ "content://com.android.externalstorage.documents/tree/abc",
165
+ {
166
+ json: true,
167
+ },
168
+ );
169
+ expect(mock.invoke).toHaveBeenCalledWith("fs.read", {
170
+ target: "content://com.android.externalstorage.documents/tree/abc",
171
+ });
172
+ });
173
+
174
+ it("cmdFsList invokes fs.list", async () => {
175
+ const mock = withMockBridge([]);
176
+ await androidCmd._cmds.cmdFsList("/sdcard/Download/", { json: true });
177
+ expect(mock.invoke).toHaveBeenCalledWith("fs.list", {
178
+ target: "/sdcard/Download/",
179
+ });
180
+ });
181
+
182
+ it("cmdA11yQuery forwards --filter", async () => {
183
+ const mock = withMockBridge({ nodes: [] });
184
+ await androidCmd._cmds.cmdA11yQuery({ json: true, filter: "button" });
185
+ expect(mock.invoke).toHaveBeenCalledWith("a11y.query", {
186
+ filter: "button",
187
+ });
188
+ });
189
+
190
+ it("cmdA11yClick forwards nodeId", async () => {
191
+ const mock = withMockBridge({ ok: true });
192
+ await androidCmd._cmds.cmdA11yClick("node-7", { json: true });
193
+ expect(mock.invoke).toHaveBeenCalledWith("a11y.click", {
194
+ nodeId: "node-7",
195
+ });
196
+ });
197
+
198
+ it("cmdA11yType forwards text", async () => {
199
+ const mock = withMockBridge({ ok: true });
200
+ await androidCmd._cmds.cmdA11yType("hello", { json: true });
201
+ expect(mock.invoke).toHaveBeenCalledWith("a11y.type", { text: "hello" });
202
+ });
203
+
204
+ it("cmdShizukuExec forwards cmd", async () => {
205
+ const mock = withMockBridge({ stdout: "ok" });
206
+ await androidCmd._cmds.cmdShizukuExec("pm list packages", { json: true });
207
+ expect(mock.invoke).toHaveBeenCalledWith("shizuku.exec", {
208
+ cmd: "pm list packages",
209
+ });
210
+ });
211
+
212
+ it("cmdRootExec forwards cmd", async () => {
213
+ const mock = withMockBridge({ stdout: "" });
214
+ await androidCmd._cmds.cmdRootExec("ls /data/data", { json: true });
215
+ expect(mock.invoke).toHaveBeenCalledWith("root.exec", {
216
+ cmd: "ls /data/data",
217
+ });
218
+ });
219
+
220
+ it("cmdPerms forwards permission name", async () => {
221
+ const mock = withMockBridge({ granted: false });
222
+ await androidCmd._cmds.cmdPerms("READ_CONTACTS", { json: true });
223
+ expect(mock.invoke).toHaveBeenCalledWith("perms.check", {
224
+ name: "READ_CONTACTS",
225
+ });
226
+ });
227
+ });
228
+
229
+ // ─── cmd routing — bridge error path ──────────────────────────────────
230
+
231
+ describe("cc android commands — error path", () => {
232
+ it("propagates ANDROID_BRIDGE_NOT_AVAILABLE as JSON when --json", async () => {
233
+ withMockBridge(
234
+ new bridge.AndroidBridgeUnavailableError("test-not-on-android"),
235
+ );
236
+ await expect(
237
+ androidCmd._cmds.cmdContactsPull({ json: true }),
238
+ ).rejects.toThrow(/__exit:1/);
239
+ const out = JSON.parse(logSpy.mock.calls[0][0]);
240
+ expect(out.code).toBe("ANDROID_BRIDGE_NOT_AVAILABLE");
241
+ expect(out.error).toMatch(/test-not-on-android/);
242
+ });
243
+
244
+ it("propagates error human-readable on non-JSON", async () => {
245
+ withMockBridge(new bridge.AndroidBridgeUnavailableError("nope"));
246
+ await expect(androidCmd._cmds.cmdSmsPull({ json: false })).rejects.toThrow(
247
+ /__exit:1/,
248
+ );
249
+ expect(errSpy).toHaveBeenCalled();
250
+ });
251
+
252
+ it("non-bridge errors still surface", async () => {
253
+ withMockBridge(new Error("network timeout"));
254
+ await expect(androidCmd._cmds.cmdAppList({ json: true })).rejects.toThrow(
255
+ /__exit:1/,
256
+ );
257
+ const out = JSON.parse(logSpy.mock.calls[0][0]);
258
+ expect(out.error).toMatch(/network timeout/);
259
+ });
260
+ });
@@ -0,0 +1,284 @@
1
+ /**
2
+ * `cc android` — Android-side data + control commands.
3
+ *
4
+ * Plan A Sub-Phase A7 scaffold. Exposes the bridge surface from
5
+ * `lib/cc-android-bridge.js` as commander subcommands. Method shapes mirror
6
+ * `docs/design/Personal_Data_Hub_Android_Standalone_Cc.md` §6.1.
7
+ *
8
+ * **v0.1 status**: native JNI bridge (`cc-android-bridge.node`) not yet
9
+ * bundled (A6 in progress). On non-Android hosts every command exits non-zero
10
+ * with `ANDROID_BRIDGE_NOT_AVAILABLE`. On Android with a missing binding,
11
+ * same error with a different reason string. This lets the surface land and
12
+ * be tested ahead of the JNI shipping work.
13
+ *
14
+ * All commands support `--json` for scripting and exit non-zero on error.
15
+ */
16
+
17
+ import chalk from "chalk";
18
+ import { logger } from "../lib/logger.js";
19
+ import * as bridgeModule from "../lib/cc-android-bridge.js";
20
+
21
+ function printJson(obj) {
22
+ console.log(JSON.stringify(obj, null, 2));
23
+ }
24
+
25
+ function fail(err, asJson) {
26
+ const msg = err && err.message ? err.message : String(err);
27
+ const code = err && err.code ? err.code : null;
28
+ if (asJson) {
29
+ printJson({ error: msg, code });
30
+ } else {
31
+ logger.error(chalk.red(`✗ ${msg}`));
32
+ }
33
+ process.exit(1);
34
+ }
35
+
36
+ async function run(method, params, options) {
37
+ try {
38
+ const result = await _deps.bridge.invoke(method, params);
39
+ if (options.json) {
40
+ printJson(result);
41
+ } else {
42
+ console.log(JSON.stringify(result, null, 2));
43
+ }
44
+ } catch (err) {
45
+ fail(err, options.json);
46
+ }
47
+ }
48
+
49
+ // ─── caps ────────────────────────────────────────────────────────────
50
+
51
+ async function cmdCaps(options) {
52
+ const capsResult = _deps.bridge.caps();
53
+ if (options.json) {
54
+ printJson(capsResult);
55
+ return;
56
+ }
57
+ if (capsResult.available) {
58
+ logger.log(chalk.green("✓ android bridge available"));
59
+ } else {
60
+ logger.log(
61
+ chalk.yellow(`⚠ android bridge unavailable: ${capsResult.reason}`),
62
+ );
63
+ }
64
+ }
65
+
66
+ // ─── contacts / sms / calls — P1 ContentResolver ─────────────────────
67
+
68
+ function cmdContactsPull(options) {
69
+ return run(
70
+ "contacts.query",
71
+ { since: options.since ? Number(options.since) : undefined },
72
+ options,
73
+ );
74
+ }
75
+ function cmdSmsPull(options) {
76
+ return run(
77
+ "sms.query",
78
+ { since: options.since ? Number(options.since) : undefined },
79
+ options,
80
+ );
81
+ }
82
+ function cmdCallsPull(options) {
83
+ return run(
84
+ "calls.query",
85
+ { since: options.since ? Number(options.since) : undefined },
86
+ options,
87
+ );
88
+ }
89
+
90
+ // ─── app — PackageManager ────────────────────────────────────────────
91
+
92
+ function cmdAppList(options) {
93
+ return run("app.list", { includeSystem: !!options.system }, options);
94
+ }
95
+ function cmdAppLaunch(pkg, options) {
96
+ return run("app.launch", { pkg }, options);
97
+ }
98
+ function cmdAppIntent(pkg, action, options) {
99
+ const extras = {};
100
+ if (Array.isArray(options.extra)) {
101
+ for (const kv of options.extra) {
102
+ const idx = kv.indexOf("=");
103
+ if (idx > 0) extras[kv.slice(0, idx)] = kv.slice(idx + 1);
104
+ }
105
+ }
106
+ return run("app.intent", { pkg, action, extras }, options);
107
+ }
108
+
109
+ // ─── fs — SAF / sandbox ──────────────────────────────────────────────
110
+
111
+ function cmdFsRead(target, options) {
112
+ return run("fs.read", { target }, options);
113
+ }
114
+ function cmdFsList(target, options) {
115
+ return run("fs.list", { target }, options);
116
+ }
117
+
118
+ // ─── a11y — Accessibility Service ────────────────────────────────────
119
+
120
+ function cmdA11yQuery(options) {
121
+ return run("a11y.query", { filter: options.filter }, options);
122
+ }
123
+ function cmdA11yClick(nodeId, options) {
124
+ return run("a11y.click", { nodeId }, options);
125
+ }
126
+ function cmdA11yType(text, options) {
127
+ return run("a11y.type", { text }, options);
128
+ }
129
+
130
+ // ─── shizuku / root ──────────────────────────────────────────────────
131
+
132
+ function cmdShizukuExec(cmd, options) {
133
+ return run("shizuku.exec", { cmd }, options);
134
+ }
135
+ function cmdRootExec(cmd, options) {
136
+ return run("root.exec", { cmd }, options);
137
+ }
138
+
139
+ // ─── perms ────────────────────────────────────────────────────────────
140
+
141
+ function cmdPerms(name, options) {
142
+ return run("perms.check", { name }, options);
143
+ }
144
+
145
+ // ─── Commander wire-up ───────────────────────────────────────────────
146
+
147
+ export function registerAndroidCommand(program) {
148
+ const android = program
149
+ .command("android")
150
+ .description(
151
+ "Android-native bridge: ContentResolver / SAF / Accessibility / Shizuku / root (Plan A A7)",
152
+ );
153
+
154
+ android
155
+ .command("caps")
156
+ .description("Probe what bridge capabilities are available on this device")
157
+ .option("--json", "Output JSON")
158
+ .action(cmdCaps);
159
+
160
+ const contacts = android
161
+ .command("contacts")
162
+ .description("ContentResolver: contacts");
163
+ contacts
164
+ .command("pull")
165
+ .description("Pull contacts (READ_CONTACTS runtime permission required)")
166
+ .option("--since <ms>", "Watermark — only newer entries")
167
+ .option("--json", "Output JSON")
168
+ .action(cmdContactsPull);
169
+
170
+ const sms = android.command("sms").description("ContentResolver: SMS");
171
+ sms
172
+ .command("pull")
173
+ .description("Pull SMS (READ_SMS runtime permission required)")
174
+ .option("--since <ms>", "Watermark")
175
+ .option("--json", "Output JSON")
176
+ .action(cmdSmsPull);
177
+
178
+ const calls = android
179
+ .command("calls")
180
+ .description("ContentResolver: call log");
181
+ calls
182
+ .command("pull")
183
+ .description("Pull call log (READ_CALL_LOG runtime permission required)")
184
+ .option("--since <ms>", "Watermark")
185
+ .option("--json", "Output JSON")
186
+ .action(cmdCallsPull);
187
+
188
+ const app = android
189
+ .command("app")
190
+ .description("PackageManager: app inventory + Intent");
191
+ app
192
+ .command("list")
193
+ .description("List installed packages")
194
+ .option("--system", "Include system packages")
195
+ .option("--json", "Output JSON")
196
+ .action(cmdAppList);
197
+ app
198
+ .command("launch <pkg>")
199
+ .description("Launch app by package id (default activity)")
200
+ .option("--json", "Output JSON")
201
+ .action(cmdAppLaunch);
202
+ app
203
+ .command("intent <pkg> <action>")
204
+ .description("Fire an Intent at <pkg> with <action>")
205
+ .option(
206
+ "--extra <kv...>",
207
+ "Repeatable extras as KEY=VAL (e.g. --extra foo=bar --extra n=1)",
208
+ )
209
+ .option("--json", "Output JSON")
210
+ .action(cmdAppIntent);
211
+
212
+ const fs = android.command("fs").description("SAF / sandbox filesystem");
213
+ fs.command("read <target>")
214
+ .description("Read file (sandbox path OR SAF tree URI)")
215
+ .option("--json", "Output JSON")
216
+ .action(cmdFsRead);
217
+ fs.command("list <target>")
218
+ .description("List directory")
219
+ .option("--json", "Output JSON")
220
+ .action(cmdFsList);
221
+
222
+ const a11y = android.command("a11y").description("Accessibility Service");
223
+ a11y
224
+ .command("query")
225
+ .description("Dump current screen's node tree")
226
+ .option("--filter <css>", "css-like filter")
227
+ .option("--json", "Output JSON")
228
+ .action(cmdA11yQuery);
229
+ a11y
230
+ .command("click <nodeId>")
231
+ .description("Click a node")
232
+ .option("--json", "Output JSON")
233
+ .action(cmdA11yClick);
234
+ a11y
235
+ .command("type <text>")
236
+ .description("Type text into focused field")
237
+ .option("--json", "Output JSON")
238
+ .action(cmdA11yType);
239
+
240
+ const shizuku = android
241
+ .command("shizuku")
242
+ .description("Shizuku ADB-like privileges");
243
+ shizuku
244
+ .command("exec <cmd>")
245
+ .description("Run shell via Shizuku")
246
+ .option("--json", "Output JSON")
247
+ .action(cmdShizukuExec);
248
+
249
+ const root = android.command("root").description("Magisk root su");
250
+ root
251
+ .command("exec <cmd>")
252
+ .description("Run shell as root")
253
+ .option("--json", "Output JSON")
254
+ .action(cmdRootExec);
255
+
256
+ android
257
+ .command("perms <name>")
258
+ .description("Check (and request) a runtime permission")
259
+ .option("--json", "Output JSON")
260
+ .action(cmdPerms);
261
+ }
262
+
263
+ // _deps injection seam — tests reach in and replace `bridge` with a mock so
264
+ // the cmd* functions exercise the routing code without a real bridge.
265
+ export const _deps = { bridge: bridgeModule };
266
+
267
+ // Test-only exports.
268
+ export const _cmds = {
269
+ cmdCaps,
270
+ cmdContactsPull,
271
+ cmdSmsPull,
272
+ cmdCallsPull,
273
+ cmdAppList,
274
+ cmdAppLaunch,
275
+ cmdAppIntent,
276
+ cmdFsRead,
277
+ cmdFsList,
278
+ cmdA11yQuery,
279
+ cmdA11yClick,
280
+ cmdA11yType,
281
+ cmdShizukuExec,
282
+ cmdRootExec,
283
+ cmdPerms,
284
+ };
@@ -191,6 +191,9 @@ async function cmdSyncAdapter(name, options) {
191
191
  if (options.since) opts.since = Number(options.since);
192
192
  if (options.until) opts.until = Number(options.until);
193
193
  if (options.limit) opts.limit = Number(options.limit);
194
+ // Plan A v0.1 — system-data-android needs a snapshot file path. Generic
195
+ // pass-through so other input-driven adapters reuse the same flag.
196
+ if (options.input) opts.inputPath = String(options.input);
194
197
  const report = await hub.registry.syncAdapter(name, opts);
195
198
  if (spinner) spinner.succeed(`synced ${name}`);
196
199
  if (options.json) {
@@ -566,13 +569,25 @@ async function cmdWechatEnvProbe(options) {
566
569
  return;
567
570
  }
568
571
  logger.log(chalk.bold("WeChat env-probe:"));
569
- logger.log(` ${probe.ok ? chalk.green("✓") : chalk.red("✗")} suggested: ${chalk.cyan(probe.suggestedKeyProvider)}`);
570
- logger.log(` device: ${probe.device.reachable ? chalk.green("reachable") : chalk.red("unreachable")}${probe.device.serial ? " (" + probe.device.serial + ")" : ""} abi=${probe.device.abi || "?"}`);
571
- logger.log(` root: ${probe.root.detected ? chalk.green("yes") : chalk.gray("no")} magisk=${probe.root.magiskInstalled ? "yes" : "no"}`);
572
- logger.log(` frida-server: ${probe.frida.serverRunning ? chalk.green("running") : chalk.gray("not running")}${probe.frida.port ? " :" + probe.frida.port : ""}`);
573
- logger.log(` wechat: ${probe.wechat.installed ? chalk.green(probe.wechat.versionName) : chalk.gray("not installed")}`);
574
- for (const reason of probe.reasons || []) logger.log(` · ${chalk.gray(reason)}`);
575
- for (const w of probe.warnings || []) logger.log(` ${chalk.yellow("!")} ${chalk.yellow(w)}`);
572
+ logger.log(
573
+ ` ${probe.ok ? chalk.green("") : chalk.red("")} suggested: ${chalk.cyan(probe.suggestedKeyProvider)}`,
574
+ );
575
+ logger.log(
576
+ ` device: ${probe.device.reachable ? chalk.green("reachable") : chalk.red("unreachable")}${probe.device.serial ? " (" + probe.device.serial + ")" : ""} abi=${probe.device.abi || "?"}`,
577
+ );
578
+ logger.log(
579
+ ` root: ${probe.root.detected ? chalk.green("yes") : chalk.gray("no")} magisk=${probe.root.magiskInstalled ? "yes" : "no"}`,
580
+ );
581
+ logger.log(
582
+ ` frida-server: ${probe.frida.serverRunning ? chalk.green("running") : chalk.gray("not running")}${probe.frida.port ? " :" + probe.frida.port : ""}`,
583
+ );
584
+ logger.log(
585
+ ` wechat: ${probe.wechat.installed ? chalk.green(probe.wechat.versionName) : chalk.gray("not installed")}`,
586
+ );
587
+ for (const reason of probe.reasons || [])
588
+ logger.log(` · ${chalk.gray(reason)}`);
589
+ for (const w of probe.warnings || [])
590
+ logger.log(` ${chalk.yellow("!")} ${chalk.yellow(w)}`);
576
591
  } catch (err) {
577
592
  fail(null, err, options.json);
578
593
  }
@@ -589,16 +604,21 @@ async function cmdWechatRegister(options) {
589
604
  dbPath: options.db || null,
590
605
  wechatDataPath: options.wechatDataPath || null,
591
606
  keyProviderOverride: options.forceProvider || null,
592
- fridaOpts: options.fridaDeviceId ? { deviceId: options.fridaDeviceId } : null,
607
+ fridaOpts: options.fridaDeviceId
608
+ ? { deviceId: options.fridaDeviceId }
609
+ : null,
593
610
  });
594
611
  if (options.json) {
595
612
  printJson(r);
596
613
  return;
597
614
  }
598
615
  if (!r.ok) {
599
- logger.error(chalk.red(`✗ ${r.reason || "register failed"}: ${r.message || ""}`));
616
+ logger.error(
617
+ chalk.red(`✗ ${r.reason || "register failed"}: ${r.message || ""}`),
618
+ );
600
619
  if (r.probe) {
601
- for (const reason of r.probe.reasons || []) logger.error(chalk.gray(" · " + reason));
620
+ for (const reason of r.probe.reasons || [])
621
+ logger.error(chalk.gray(" · " + reason));
602
622
  }
603
623
  process.exit(1);
604
624
  }
@@ -624,7 +644,9 @@ async function cmdWechatList(options) {
624
644
  }
625
645
  logger.log(chalk.bold("Registered WeChat accounts:"));
626
646
  for (const row of rows) {
627
- logger.log(` • uin=${chalk.cyan(row.uin)} provider=${row.chosenKeyProvider || "?"} db=${row.dbPath || "(none)"} regAt=${row.registeredAt ? new Date(row.registeredAt).toISOString() : "?"}`);
647
+ logger.log(
648
+ ` • uin=${chalk.cyan(row.uin)} provider=${row.chosenKeyProvider || "?"} db=${row.dbPath || "(none)"} regAt=${row.registeredAt ? new Date(row.registeredAt).toISOString() : "?"}`,
649
+ );
628
650
  }
629
651
  } catch (err) {
630
652
  fail(null, err, options.json);
@@ -644,8 +666,12 @@ async function cmdWechatUnregister(uin, options) {
644
666
  logger.error(chalk.red(`✗ ${r.reason || "unregister failed"}`));
645
667
  process.exit(1);
646
668
  }
647
- if (r.removed) logger.log(chalk.green(`✓ removed wechat account (uin=${uin})`));
648
- else logger.log(chalk.gray(`(uin=${uin} was not registered — nothing removed)`));
669
+ if (r.removed)
670
+ logger.log(chalk.green(`✓ removed wechat account (uin=${uin})`));
671
+ else
672
+ logger.log(
673
+ chalk.gray(`(uin=${uin} was not registered — nothing removed)`),
674
+ );
649
675
  } catch (err) {
650
676
  fail(null, err, options.json);
651
677
  }
@@ -712,6 +738,10 @@ export function registerHubCommand(program) {
712
738
  .option("--since <ms>", "Override watermark — sync from this unix-ms")
713
739
  .option("--until <ms>", "Stop at this unix-ms")
714
740
  .option("--limit <n>", "Cap ingested events")
741
+ .option(
742
+ "--input <path>",
743
+ "Path to a snapshot file produced by a UI layer (system-data-android, etc.)",
744
+ )
715
745
  .option("--json", "Output JSON")
716
746
  .action(cmdSyncAdapter);
717
747
 
@@ -833,7 +863,9 @@ export function registerHubCommand(program) {
833
863
 
834
864
  wechat
835
865
  .command("env-probe")
836
- .description("Probe attached Android device for adb / root / frida-server / WeChat version")
866
+ .description(
867
+ "Probe attached Android device for adb / root / frida-server / WeChat version",
868
+ )
837
869
  .option("--json", "Output JSON")
838
870
  .action(cmdWechatEnvProbe);
839
871
 
@@ -844,9 +876,15 @@ export function registerHubCommand(program) {
844
876
  )
845
877
  .requiredOption("--uin <id>", "WeChat numeric UIN (≤ 8.0) or wxid (8.0+)")
846
878
  .option("--db <path>", "Local path to the already-pulled EnMicroMsg.db")
847
- .option("--wechat-data-path <dir>", "Local pulled /data/data/com.tencent.mm/ tree (required for md5 path)")
879
+ .option(
880
+ "--wechat-data-path <dir>",
881
+ "Local pulled /data/data/com.tencent.mm/ tree (required for md5 path)",
882
+ )
848
883
  .option("--force-provider <md5|frida>", "Override env-probe suggestion")
849
- .option("--frida-device-id <id>", "Frida device id (defaults to first USB device)")
884
+ .option(
885
+ "--frida-device-id <id>",
886
+ "Frida device id (defaults to first USB device)",
887
+ )
850
888
  .option("--json", "Output JSON")
851
889
  .action(cmdWechatRegister);
852
890
 
@@ -858,7 +896,9 @@ export function registerHubCommand(program) {
858
896
 
859
897
  wechat
860
898
  .command("unregister <uin>")
861
- .description("Remove a registered WeChat account (does not touch vault data)")
899
+ .description(
900
+ "Remove a registered WeChat account (does not touch vault data)",
901
+ )
862
902
  .option("--json", "Output JSON")
863
903
  .action(cmdWechatUnregister);
864
904
  }