@piecemaker-legal/piecemaker 1.1.0 → 2.0.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.
Files changed (144) hide show
  1. package/dist/assets/{abnfDiagram-VCTEODGH-C-dTfD4s.js → abnfDiagram-VCTEODGH-C2JDmlZx.js} +1 -1
  2. package/dist/assets/{arc-BwDrelvt.js → arc-XhSg5Mrj.js} +1 -1
  3. package/dist/assets/{architectureDiagram-5GKGNRK7-CuViaxCV.js → architectureDiagram-5GKGNRK7-DKREuXxk.js} +1 -1
  4. package/dist/assets/{blockDiagram-I7D4REHJ-CODUNfgC.js → blockDiagram-I7D4REHJ-CNK0Tp5d.js} +1 -1
  5. package/dist/assets/{c4Diagram-7LVT6UL2-CiQtyYDd.js → c4Diagram-7LVT6UL2-C8fJzWVp.js} +1 -1
  6. package/dist/assets/channel-DO5hoFdU.js +1 -0
  7. package/dist/assets/{chunk-2Q5K7J3B-DZjjPgAs.js → chunk-2Q5K7J3B-DlE8nLJo.js} +1 -1
  8. package/dist/assets/{chunk-5VM5RSS4-C-SPnj5I.js → chunk-5VM5RSS4-CSm-2TZ4.js} +1 -1
  9. package/dist/assets/{chunk-F27PBJKO-D5b0-fye.js → chunk-F27PBJKO-CSgnXT2c.js} +1 -1
  10. package/dist/assets/{chunk-IMKFNOWR-CWflUKd3.js → chunk-IMKFNOWR-C6bkt6C1.js} +1 -1
  11. package/dist/assets/{chunk-JWPE2WC7-DFltof8Z.js → chunk-JWPE2WC7-jUR52MBO.js} +1 -1
  12. package/dist/assets/{chunk-POPQ4Y6H-moIw94hw.js → chunk-POPQ4Y6H-D0UAL7O7.js} +1 -1
  13. package/dist/assets/{chunk-SVP7TREG--H3pige-.js → chunk-SVP7TREG-C9dtOEii.js} +1 -1
  14. package/dist/assets/{chunk-TICWLB2K-BfujErBt.js → chunk-TICWLB2K-BEzWKT39.js} +1 -1
  15. package/dist/assets/{chunk-XXDRQBXY-C5jFgw8p.js → chunk-XXDRQBXY-uD1oklVY.js} +1 -1
  16. package/dist/assets/classDiagram-ZZMXUADV-BsQtwznI.js +1 -0
  17. package/dist/assets/classDiagram-v2-VYDZK3BY-BsQtwznI.js +1 -0
  18. package/dist/assets/{cose-bilkent-JH36ORCC-CsT3oVRI.js → cose-bilkent-JH36ORCC-aGjlSdMe.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-gdpEHNLr.js → cynefin-OW5HDTMX-D3G4OYOn.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-5FMLGOSQ-v-JmI3Zj.js → cynefinDiagram-5FMLGOSQ-9swRDUnu.js} +1 -1
  21. package/dist/assets/{dagre-GXQ25YYZ-BvJ_SGF3.js → dagre-GXQ25YYZ-lvaAGdqY.js} +1 -1
  22. package/dist/assets/{diagram-S7CK7UJ4-B7EHfyrx.js → diagram-S7CK7UJ4-BaZ6YIkR.js} +1 -1
  23. package/dist/assets/{diagram-UQ7AKVKN-DX5RvUaF.js → diagram-UQ7AKVKN-COJ2Gb38.js} +1 -1
  24. package/dist/assets/{diagram-VSXAHHWV-D_PJ-E3G.js → diagram-VSXAHHWV-CYZEkbzP.js} +1 -1
  25. package/dist/assets/{diagram-VX7I27RA-CAtayL03.js → diagram-VX7I27RA-fFeQJ4ae.js} +1 -1
  26. package/dist/assets/{diagram-Z3DM3KII-BhCCqDLU.js → diagram-Z3DM3KII-B7knszDV.js} +1 -1
  27. package/dist/assets/{ebnfDiagram-PWID7BFC-BCb926kC.js → ebnfDiagram-PWID7BFC-Dk0l329Y.js} +1 -1
  28. package/dist/assets/{erDiagram-RLTQ6QDP-5Fw1sblj.js → erDiagram-RLTQ6QDP-CtpYxui8.js} +1 -1
  29. package/dist/assets/{flowDiagram-HODETNUW-DDUdOLnK.js → flowDiagram-HODETNUW-aVWueRJD.js} +1 -1
  30. package/dist/assets/{ganttDiagram-EL5Y4UJY-B4IG1idB.js → ganttDiagram-EL5Y4UJY-DfrX-B7p.js} +1 -1
  31. package/dist/assets/{gitGraphDiagram-WWUBYQGX-S6wvUCsC.js → gitGraphDiagram-WWUBYQGX-BFB1ZPF3.js} +1 -1
  32. package/dist/assets/{index-CcC0QCGH.js → index-DqRygn9E.js} +170 -169
  33. package/dist/assets/index-Ds4HaAjz.css +1 -0
  34. package/dist/assets/{infoDiagram-27XIBGKW-BNv6fEro.js → infoDiagram-27XIBGKW-CuYkzm43.js} +1 -1
  35. package/dist/assets/{ishikawaDiagram-5VMMS53U-BJZwt-TZ.js → ishikawaDiagram-5VMMS53U-EMiw8Akh.js} +1 -1
  36. package/dist/assets/{journeyDiagram-3NMN7TZE-BAGhL9Uv.js → journeyDiagram-3NMN7TZE-DCoLV1f8.js} +1 -1
  37. package/dist/assets/{kanban-definition-UXKFOSKX-DmoLE6ku.js → kanban-definition-UXKFOSKX-CyA4Yinu.js} +1 -1
  38. package/dist/assets/{layout-ALipSwVQ.js → layout-BlaB0grA.js} +1 -1
  39. package/dist/assets/{linear-BJkOgP0g.js → linear-DLX3dJDH.js} +1 -1
  40. package/dist/assets/{mermaid.core-C20iPvcV.js → mermaid.core-Cy51CT89.js} +6 -6
  41. package/dist/assets/{mindmap-definition-YA3MSWOX-CSI558Pw.js → mindmap-definition-YA3MSWOX-OBXjfL7I.js} +1 -1
  42. package/dist/assets/{pegDiagram-XKGWAZYB-DFtdseIb.js → pegDiagram-XKGWAZYB-C_BJjUh1.js} +1 -1
  43. package/dist/assets/{pieDiagram-E7YTZNPT-ByPKZBBy.js → pieDiagram-E7YTZNPT-jIpbr3Fi.js} +1 -1
  44. package/dist/assets/{quadrantDiagram-AXDQQJYC-tH7z7eR2.js → quadrantDiagram-AXDQQJYC-C1A15xI5.js} +1 -1
  45. package/dist/assets/{railroadDiagram-O6MQD6OU-D7ZdvS0P.js → railroadDiagram-O6MQD6OU-Rslzn1pI.js} +1 -1
  46. package/dist/assets/{requirementDiagram-BXWQKSXE-nSTqp6wv.js → requirementDiagram-BXWQKSXE-LlfURO5-.js} +1 -1
  47. package/dist/assets/{sankeyDiagram-P5KCCOFB-Bg3ivRCq.js → sankeyDiagram-P5KCCOFB-B5BPF4-O.js} +1 -1
  48. package/dist/assets/{sequenceDiagram-WJ2MYXX4-BFs4BVKR.js → sequenceDiagram-WJ2MYXX4-orYZe9zX.js} +1 -1
  49. package/dist/assets/{sizeCapture-INFHLROL-DuSN3zyt.js → sizeCapture-INFHLROL-Co-K5Cfb.js} +1 -1
  50. package/dist/assets/{stateDiagram-D77RDMKH---hK4wFR.js → stateDiagram-D77RDMKH-COEx1aH2.js} +1 -1
  51. package/dist/assets/{stateDiagram-v2-MP3YSRHH-DxBEemHS.js → stateDiagram-v2-MP3YSRHH-DaeTZvmy.js} +1 -1
  52. package/dist/assets/{swimlanes-42K2YHIH-C6RXO1Rv.js → swimlanes-42K2YHIH-BHQaDhfZ.js} +1 -1
  53. package/dist/assets/swimlanesDiagram-VR7AAH4N-BHW8q5M5.js +8 -0
  54. package/dist/assets/{timeline-definition-24CTP7MA-Ci0Toxgn.js → timeline-definition-24CTP7MA-COqCRwGX.js} +1 -1
  55. package/dist/assets/{vennDiagram-4TSXK5OY-CKF_VmKX.js → vennDiagram-4TSXK5OY-nym5XSnr.js} +1 -1
  56. package/dist/assets/{wardleyDiagram-VM6X3IG4-Xao4cR_d.js → wardleyDiagram-VM6X3IG4-DEPPey_W.js} +1 -1
  57. package/dist/assets/{xychartDiagram-S5SC5T6Z-BAZUt04h.js → xychartDiagram-S5SC5T6Z-uSJ404kL.js} +1 -1
  58. package/dist/index.html +2 -2
  59. package/dist/sw.js +1 -1
  60. package/dist-server/server/index.js +3 -1
  61. package/dist-server/server/index.js.map +1 -1
  62. package/dist-server/server/modules/cli/desktop-shortcut.js +80 -0
  63. package/dist-server/server/modules/cli/desktop-shortcut.js.map +1 -0
  64. package/dist-server/server/modules/cli/pwa-launcher.js +27 -4
  65. package/dist-server/server/modules/cli/pwa-launcher.js.map +1 -1
  66. package/dist-server/server/piecemaker/index.js +7 -3
  67. package/dist-server/server/piecemaker/index.js.map +1 -1
  68. package/dist-server/server/piecemaker/knowledge/index.js +13 -6
  69. package/dist-server/server/piecemaker/knowledge/index.js.map +1 -1
  70. package/dist-server/server/piecemaker/knowledge/local-routes.js +55 -0
  71. package/dist-server/server/piecemaker/knowledge/local-routes.js.map +1 -0
  72. package/dist-server/server/piecemaker/knowledge/scan-jobs.js +3 -1
  73. package/dist-server/server/piecemaker/knowledge/scan-jobs.js.map +1 -1
  74. package/dist-server/server/piecemaker/library/index.js +14 -41
  75. package/dist-server/server/piecemaker/library/index.js.map +1 -1
  76. package/dist-server/server/piecemaker/library/store.js +11 -3
  77. package/dist-server/server/piecemaker/library/store.js.map +1 -1
  78. package/dist-server/server/piecemaker/library/workspace-installation.js +26 -3
  79. package/dist-server/server/piecemaker/library/workspace-installation.js.map +1 -1
  80. package/dist-server/server/piecemaker/timesheet/service.js +8 -7
  81. package/dist-server/server/piecemaker/timesheet/service.js.map +1 -1
  82. package/dist-server/server/piecemaker/timesheet/store.js +5 -3
  83. package/dist-server/server/piecemaker/timesheet/store.js.map +1 -1
  84. package/package.json +1 -1
  85. package/product.config.json +3 -0
  86. package/scripts/piecemaker/build-app-icon.py +88 -0
  87. package/scripts/piecemaker/cli/lib/app-window.mjs +47 -0
  88. package/scripts/piecemaker/cli/lib/piecemaker-anonymizer.mjs +2 -2
  89. package/scripts/piecemaker/cli/lib/pwa.mjs +10 -13
  90. package/scripts/piecemaker/cli/piecemaker.mjs +15 -1
  91. package/scripts/piecemaker/hooks/proxy-guard.mjs +19 -54
  92. package/scripts/release/assert-piecemaker-version.mjs +25 -0
  93. package/server/index.ts +4 -1
  94. package/server/modules/cli/desktop-shortcut.ts +89 -0
  95. package/server/modules/cli/pwa-launcher.ts +29 -4
  96. package/server/piecemaker/anonymizer/client-config.cjs +45 -162
  97. package/server/piecemaker/anonymizer/hudsucker-fixture.cjs +127 -0
  98. package/server/piecemaker/anonymizer/hudsucker-proxy/Cargo.lock +2396 -0
  99. package/server/piecemaker/anonymizer/hudsucker-proxy/Cargo.toml +14 -0
  100. package/server/piecemaker/anonymizer/hudsucker-proxy/src/main.rs +501 -0
  101. package/server/piecemaker/anonymizer/providers.cjs +14 -135
  102. package/server/piecemaker/anonymizer/rewriter-bridge.cjs +221 -0
  103. package/server/piecemaker/anonymizer/service.cjs +144 -28
  104. package/server/piecemaker/index.ts +8 -3
  105. package/server/piecemaker/knowledge/index.ts +14 -6
  106. package/server/piecemaker/knowledge/local-routes.ts +72 -0
  107. package/server/piecemaker/knowledge/scan-jobs.ts +4 -1
  108. package/server/piecemaker/library/index.ts +14 -34
  109. package/server/piecemaker/library/store.ts +11 -3
  110. package/server/piecemaker/library/workspace-installation.ts +20 -3
  111. package/server/piecemaker/timesheet/service.ts +9 -7
  112. package/server/piecemaker/timesheet/store.ts +5 -3
  113. package/server/piecemaker/vendor/installer/assets/claude-hooks/piecemaker-guard-secrets.mjs +0 -1
  114. package/server/piecemaker/vendor/installer/assets/piecemaker.icns +0 -0
  115. package/server/piecemaker/vendor/installer/assets/piecemaker.ico +0 -0
  116. package/server/piecemaker/vendor/installer/bin/piecemaker.mjs +56 -206
  117. package/server/piecemaker/vendor/installer/lib/commandes.mjs +2 -4
  118. package/server/piecemaker/vendor/installer/lib/conversion-client.mjs +74 -0
  119. package/server/piecemaker/vendor/installer/lib/service.mjs +4 -358
  120. package/server/piecemaker/vendor/installer/lib/state.mjs +0 -6
  121. package/server/piecemaker/vendor/installer/steps/03-python-gliner.mjs +4 -4
  122. package/server/piecemaker/vendor/installer/steps/09-claude-assets.mjs +0 -31
  123. package/server/piecemaker/vendor/piecemaker-plugin/scripts/statusline.mjs +3 -4
  124. package/server/piecemaker/vendor/websocket-server/admin-routes.cjs +1 -49
  125. package/server/piecemaker/vendor/websocket-server/claude-hooks.cjs +14 -12
  126. package/server/piecemaker/vendor/websocket-server/originals-pipeline.cjs +17 -476
  127. package/server/piecemaker/vendor/websocket-server/scripts/convert_and_scan_pipeline.py +2 -2
  128. package/server/piecemaker/vendor/websocket-server/scripts/mineru_improved_1.py +11 -2
  129. package/server/piecemaker/vendor/websocket-server/scripts/presidio-gliner/scanner_worker.py +13 -37
  130. package/server/piecemaker/vendor/websocket-server/scripts/requirements.txt +1 -7
  131. package/server/piecemaker/vendor/websocket-server/scripts/warmup.py +14 -85
  132. package/dist/assets/channel-DIjrikYC.js +0 -1
  133. package/dist/assets/classDiagram-ZZMXUADV-Yagpznfi.js +0 -1
  134. package/dist/assets/classDiagram-v2-VYDZK3BY-Yagpznfi.js +0 -1
  135. package/dist/assets/index-D_Dc_nQv.css +0 -1
  136. package/dist/assets/swimlanesDiagram-VR7AAH4N-BC3fwmoQ.js +0 -8
  137. package/server/piecemaker/anonymizer/proxy.cjs +0 -365
  138. package/server/piecemaker/vendor/installer/assets/PieceMakerApp.swift +0 -189
  139. package/server/piecemaker/vendor/installer/lib/desktop-launcher.mjs +0 -305
  140. package/server/piecemaker/vendor/installer/steps/05-certificats.mjs +0 -171
  141. package/server/piecemaker/vendor/installer/steps/14-mxc-sandbox.mjs +0 -271
  142. package/server/piecemaker/vendor/installer/steps/15-pwa-desktop.mjs +0 -64
  143. package/server/piecemaker/vendor/installer/templates/root-CLAUDE.md +0 -56
  144. package/server/piecemaker/vendor/installer/templates/workspace-CLAUDE.md +0 -52
@@ -0,0 +1,89 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+
6
+ type ShortcutOutput = {
7
+ log(message: string): void;
8
+ };
9
+
10
+ const SHORTCUT_NAME = 'PieceMaker';
11
+
12
+ function desktopDirectory(): string {
13
+ return process.env.PIECEMAKER_DESKTOP_DIR || path.join(os.homedir(), 'Desktop');
14
+ }
15
+
16
+ function launchCommand(): { executable: string; argument: string } {
17
+ return { executable: process.execPath, argument: process.argv[1] || '' };
18
+ }
19
+
20
+ function createMacShortcut(desktop: string): string | null {
21
+ const { executable, argument } = launchCommand();
22
+ if (!argument) return null;
23
+ const bundle = path.join(desktop, `${SHORTCUT_NAME}.app`);
24
+ if (fs.existsSync(bundle)) return null;
25
+
26
+ const macOsDirectory = path.join(bundle, 'Contents', 'MacOS');
27
+ fs.mkdirSync(macOsDirectory, { recursive: true });
28
+ fs.writeFileSync(
29
+ path.join(bundle, 'Contents', 'Info.plist'),
30
+ `<?xml version="1.0" encoding="UTF-8"?>
31
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
32
+ <plist version="1.0">
33
+ <dict>
34
+ <key>CFBundleName</key><string>${SHORTCUT_NAME}</string>
35
+ <key>CFBundleIdentifier</key><string>legal.piecemaker.launcher</string>
36
+ <key>CFBundleExecutable</key><string>${SHORTCUT_NAME}</string>
37
+ <key>CFBundlePackageType</key><string>APPL</string>
38
+ <key>LSUIElement</key><true/>
39
+ </dict>
40
+ </plist>
41
+ `,
42
+ );
43
+
44
+ const launcher = path.join(macOsDirectory, SHORTCUT_NAME);
45
+ fs.writeFileSync(launcher, `#!/bin/sh\nexec ${JSON.stringify(executable)} ${JSON.stringify(argument)} start\n`);
46
+ fs.chmodSync(launcher, 0o755);
47
+ return bundle;
48
+ }
49
+
50
+ function createWindowsShortcut(desktop: string): string | null {
51
+ const { executable, argument } = launchCommand();
52
+ if (!argument) return null;
53
+ const shortcut = path.join(desktop, `${SHORTCUT_NAME}.lnk`);
54
+ if (fs.existsSync(shortcut)) return null;
55
+
56
+ const script = [
57
+ '$shell = New-Object -ComObject WScript.Shell',
58
+ `$shortcut = $shell.CreateShortcut(${JSON.stringify(shortcut)})`,
59
+ `$shortcut.TargetPath = ${JSON.stringify(executable)}`,
60
+ `$shortcut.Arguments = ${JSON.stringify(`${JSON.stringify(argument)} start`)}`,
61
+ `$shortcut.WorkingDirectory = ${JSON.stringify(path.dirname(executable))}`,
62
+ '$shortcut.WindowStyle = 7',
63
+ `$shortcut.Description = ${JSON.stringify('Ouvre PieceMaker')}`,
64
+ '$shortcut.Save()',
65
+ ].join('; ');
66
+ execFileSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script], { stdio: 'ignore' });
67
+ return shortcut;
68
+ }
69
+
70
+ /**
71
+ * Creates the PieceMaker desktop icon once, right after a fresh install, so the
72
+ * application can be reopened without a terminal. Existing shortcuts are left
73
+ * untouched and any failure is reported without interrupting the launch.
74
+ */
75
+ export function ensureDesktopShortcut(output: ShortcutOutput): void {
76
+ if (process.platform !== 'darwin' && process.platform !== 'win32') return;
77
+
78
+ const desktop = desktopDirectory();
79
+ if (!fs.existsSync(desktop)) return;
80
+
81
+ try {
82
+ const created = process.platform === 'darwin'
83
+ ? createMacShortcut(desktop)
84
+ : createWindowsShortcut(desktop);
85
+ if (created) output.log(`[OK] PieceMaker desktop icon created at ${created}`);
86
+ } catch {
87
+ output.log('[WARN] Could not create the PieceMaker desktop icon.');
88
+ }
89
+ }
@@ -4,6 +4,8 @@ import fs from 'node:fs';
4
4
  import os from 'node:os';
5
5
  import path from 'node:path';
6
6
 
7
+ import { ensureDesktopShortcut } from './desktop-shortcut.js';
8
+
7
9
  type PwaOutput = {
8
10
  log(message: string): void;
9
11
  };
@@ -102,7 +104,22 @@ async function waitForServer(url: string): Promise<boolean> {
102
104
  return false;
103
105
  }
104
106
 
105
- /** Starts PieceMaker in an isolated Chromium app window after its backend is ready. */
107
+ const SHUTDOWN_GRACE_MS = 10_000;
108
+ const DELEGATED_LAUNCH_MS = 3_000;
109
+
110
+ function shutdownAfterWindowClosed(output: PwaOutput): void {
111
+ output.log('[OK] PieceMaker window closed; stopping the local server.');
112
+ const forced = setTimeout(() => process.exit(0), SHUTDOWN_GRACE_MS);
113
+ forced.unref();
114
+ if (process.listenerCount('SIGTERM') > 0) process.emit('SIGTERM', 'SIGTERM');
115
+ else process.exit(0);
116
+ }
117
+
118
+ /**
119
+ * Starts PieceMaker in an isolated Chromium app window after its backend is
120
+ * ready, and ties the server's lifetime to that window: closing it shuts the
121
+ * local server down through the same graceful path as SIGTERM.
122
+ */
106
123
  export async function launchPwa(output: PwaOutput = DEFAULT_OUTPUT): Promise<void> {
107
124
  const url = pwaUrl();
108
125
  if (!(await waitForServer(url))) {
@@ -116,16 +133,24 @@ export async function launchPwa(output: PwaOutput = DEFAULT_OUTPUT): Promise<voi
116
133
  return;
117
134
  }
118
135
 
136
+ ensureDesktopShortcut(output);
137
+
119
138
  const profileDirectory = pwaProfileDirectory();
120
139
  fs.mkdirSync(profileDirectory, { recursive: true });
140
+ const openedAt = Date.now();
121
141
  const child = spawn(browser, [`--app=${url}`, `--user-data-dir=${profileDirectory}`], {
122
- detached: true,
123
142
  stdio: 'ignore',
124
143
  windowsHide: true,
125
144
  });
126
145
  child.on('error', () => {
127
146
  output.log(`[WARN] Could not open the PieceMaker PWA; open ${url} manually.`);
128
147
  });
129
- child.unref();
130
- output.log(`[OK] PieceMaker PWA opened at ${url}`);
148
+ child.on('exit', () => {
149
+ if (Date.now() - openedAt < DELEGATED_LAUNCH_MS) {
150
+ output.log(`[WARN] The PieceMaker window is owned by another browser process; the server keeps running.`);
151
+ return;
152
+ }
153
+ shutdownAfterWindowClosed(output);
154
+ });
155
+ output.log(`[OK] PieceMaker PWA opened at ${url} — closing the window stops the server.`);
131
156
  }
@@ -1,19 +1,7 @@
1
- /**
2
- * Écriture atomique des configurations Claude Code et Codex pour les faire
3
- * passer par le proxy PII de ce dépôt (`./proxy.cjs`).
4
- *
5
- * Propre à ce fichier, sans dépendance vendor : l'identifiant de bloc
6
- * `piecemaker_proxy` et les marqueurs qui l'entourent n'appartiennent qu'à
7
- * ce proxy. Le fournisseur Codex géré n'a jamais parlé que HTTP Responses.
8
- */
9
1
  const fs = require('node:fs');
10
2
  const os = require('node:os');
11
3
  const path = require('node:path');
12
4
 
13
- const CODEX_PROVIDER_ID = 'piecemaker_proxy';
14
- const CODEX_BLOCK_START = '# >>> PieceMaker Proxy PII (géré automatiquement)';
15
- const CODEX_BLOCK_END = '# <<< PieceMaker Proxy PII';
16
-
17
5
  function atomicWrite(file, content, mode = 0o600) {
18
6
  fs.mkdirSync(path.dirname(file), { recursive: true });
19
7
  const temporary = `${file}.piecemaker-${process.pid}-${Date.now()}`;
@@ -21,18 +9,28 @@ function atomicWrite(file, content, mode = 0o600) {
21
9
  fs.renameSync(temporary, file);
22
10
  }
23
11
 
24
- /** Une base que PieceMaker a écrite — donc que PieceMaker peut réécrire ou retirer. */
25
- function isOwnLoopbackUrl(value, suffix) {
12
+ const PROXY_ENV = {
13
+ HTTPS_PROXY: (proxyUrl) => proxyUrl,
14
+ HTTP_PROXY: (proxyUrl) => proxyUrl,
15
+ ALL_PROXY: (proxyUrl) => proxyUrl,
16
+ NO_PROXY: () => 'localhost,127.0.0.1,::1',
17
+ NODE_USE_ENV_PROXY: () => '1',
18
+ NODE_EXTRA_CA_CERTS: (_proxyUrl, caFile) => caFile,
19
+ SSL_CERT_FILE: (_proxyUrl, caFile) => caFile,
20
+ CODEX_CA_CERTIFICATE: (_proxyUrl, caFile) => caFile,
21
+ REQUESTS_CA_BUNDLE: (_proxyUrl, caFile) => caFile,
22
+ };
23
+
24
+ function isLoopbackProxy(value) {
26
25
  try {
27
26
  const url = new URL(String(value || ''));
28
- return ['127.0.0.1', 'localhost'].includes(url.hostname)
29
- && url.pathname.replace(/\/$/, '') === suffix;
27
+ return ['127.0.0.1', 'localhost'].includes(url.hostname);
30
28
  } catch {
31
29
  return false;
32
30
  }
33
31
  }
34
32
 
35
- function configureClaudeCodeProxy({ baseUrl, userHome = os.homedir() } = {}) {
33
+ function configureClaudeCodeProxy({ proxyUrl, caFile, userHome = os.homedir() } = {}) {
36
34
  const settingsFile = path.join(userHome, '.claude', 'settings.json');
37
35
  let settings = {};
38
36
  try {
@@ -50,13 +48,21 @@ function configureClaudeCodeProxy({ baseUrl, userHome = os.homedir() } = {}) {
50
48
  return { configured: false, changed: false, conflict: true, file: settingsFile, reason: 'env-invalid' };
51
49
  }
52
50
  if (!settings.env) settings.env = {};
53
- const existing = settings.env.ANTHROPIC_BASE_URL;
54
- if (existing && existing !== baseUrl && !isOwnLoopbackUrl(existing, '/anthropic')) {
55
- return { configured: false, changed: false, conflict: true, file: settingsFile, reason: 'base-url-conflict' };
51
+ const existingProxy = settings.env.HTTPS_PROXY;
52
+ if (existingProxy && existingProxy !== proxyUrl && !isLoopbackProxy(existingProxy)) {
53
+ return { configured: false, changed: false, conflict: true, file: settingsFile, reason: 'proxy-conflict' };
54
+ }
55
+
56
+ let changed = false;
57
+ for (const [key, produce] of Object.entries(PROXY_ENV)) {
58
+ const value = produce(proxyUrl, caFile);
59
+ if (!value) continue;
60
+ if (settings.env[key] !== value) {
61
+ settings.env[key] = value;
62
+ changed = true;
63
+ }
56
64
  }
57
- if (existing === baseUrl) return { configured: true, changed: false, conflict: false, file: settingsFile };
58
-
59
- settings.env.ANTHROPIC_BASE_URL = baseUrl;
65
+ if (!changed) return { configured: true, changed: false, conflict: false, file: settingsFile };
60
66
  atomicWrite(settingsFile, `${JSON.stringify(settings, null, 2)}\n`);
61
67
  return { configured: true, changed: true, conflict: false, file: settingsFile };
62
68
  }
@@ -76,159 +82,36 @@ function bypassClaudeCodeProxy({ userHome = os.homedir() } = {}) {
76
82
  }
77
83
 
78
84
  const env = settings.env;
79
- if (env === undefined || (env && typeof env === 'object' && !Array.isArray(env)
80
- && !env.ANTHROPIC_BASE_URL)) {
81
- return { bypassed: true, changed: false, conflict: false, file: settingsFile };
82
- }
83
85
  if (!env || typeof env !== 'object' || Array.isArray(env)) {
86
+ if (env === undefined) return { bypassed: true, changed: false, conflict: false, file: settingsFile };
84
87
  return { bypassed: false, changed: false, conflict: true, file: settingsFile, reason: 'env-invalid' };
85
88
  }
86
- if (!isOwnLoopbackUrl(env.ANTHROPIC_BASE_URL, '/anthropic')) {
87
- return { bypassed: true, changed: false, conflict: false, file: settingsFile };
88
- }
89
+ if (!isLoopbackProxy(env.HTTPS_PROXY)) return { bypassed: true, changed: false, conflict: false, file: settingsFile };
89
90
 
90
- delete env.ANTHROPIC_BASE_URL;
91
+ let changed = false;
92
+ if (isLoopbackProxy(env.HTTPS_PROXY)) {
93
+ for (const key of Object.keys(PROXY_ENV)) {
94
+ if (env[key] !== undefined) {
95
+ delete env[key];
96
+ changed = true;
97
+ }
98
+ }
99
+ }
100
+ if (!changed) return { bypassed: true, changed: false, conflict: false, file: settingsFile };
91
101
  if (Object.keys(env).length === 0) delete settings.env;
92
102
  atomicWrite(settingsFile, `${JSON.stringify(settings, null, 2)}\n`);
93
103
  return { bypassed: true, changed: true, conflict: false, file: settingsFile };
94
104
  }
95
105
 
96
- function tomlString(value) {
97
- return JSON.stringify(String(value));
98
- }
99
-
100
- function topLevelAssignment(lines, key) {
101
- const expression = new RegExp(`^\\s*${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*=\\s*(.*?)\\s*$`);
102
- for (let index = 0; index < lines.length; index += 1) {
103
- const line = lines[index];
104
- if (/^\s*\[/.test(line)) break;
105
- const match = line.match(expression);
106
- if (match) return { index, raw: match[1] };
107
- }
108
- return null;
109
- }
110
-
111
- function parseTomlString(raw) {
112
- const value = String(raw || '').trim();
113
- if (value.startsWith('"')) {
114
- const literal = value.match(/^("(?:\\.|[^"\\])*")/)?.[1];
115
- try { return literal ? JSON.parse(literal) : null; } catch { return null; }
116
- }
117
- const literal = value.match(/^'([^']*)'/)?.[1];
118
- if (literal !== undefined) return literal;
119
- return null;
120
- }
121
-
122
- function configureCodexProxy({
123
- baseUrl,
124
- codexHome = process.env.CODEX_HOME || path.join(os.homedir(), '.codex'),
125
- } = {}) {
126
- const configFile = path.join(codexHome, 'config.toml');
127
- let content = '';
128
- try { content = fs.existsSync(configFile) ? fs.readFileSync(configFile, 'utf8') : ''; } catch {
129
- return { configured: false, changed: false, conflict: true, file: configFile, reason: 'config-unreadable' };
130
- }
131
-
132
- const lines = content.replace(/^/, '').split(/\r?\n/);
133
- const provider = topLevelAssignment(lines, 'model_provider');
134
- const providerValue = provider ? parseTomlString(provider.raw) : 'openai';
135
- if (provider && !['openai', CODEX_PROVIDER_ID].includes(providerValue)) {
136
- return { configured: false, changed: false, conflict: true, file: configFile, reason: 'provider-conflict' };
137
- }
138
-
139
- const managedTable = lines.findIndex((line) => line.trim() === `[model_providers.${CODEX_PROVIDER_ID}]`);
140
- const managedStart = lines.findIndex((line) => line.trim() === CODEX_BLOCK_START);
141
- let managedEnd = lines.findIndex((line) => line.trim() === CODEX_BLOCK_END);
142
- if (managedTable >= 0 && managedStart < 0) {
143
- return { configured: false, changed: false, conflict: true, file: configFile, reason: 'provider-table-conflict' };
144
- }
145
- if (managedStart >= 0 && managedEnd < managedStart) {
146
- let end = managedTable >= managedStart ? managedTable : managedStart;
147
- for (let index = end + 1; index < lines.length; index += 1) {
148
- if (/^\s*\[/.test(lines[index])) break;
149
- end = index;
150
- }
151
- managedEnd = end;
152
- }
153
-
154
- if (provider) {
155
- lines[provider.index] = `model_provider = ${tomlString(CODEX_PROVIDER_ID)} # géré par PieceMaker`;
156
- } else {
157
- const firstTable = lines.findIndex((line) => /^\s*\[/.test(line));
158
- lines.splice(firstTable < 0 ? 0 : firstTable, 0,
159
- '# Fournisseur Responses protégé par PieceMaker.',
160
- `model_provider = ${tomlString(CODEX_PROVIDER_ID)} # géré par PieceMaker`,
161
- '');
162
- }
163
-
164
- // Le proxy de ce dépôt parle HTTP Responses, jamais WebSocket.
165
- const block = [
166
- CODEX_BLOCK_START,
167
- `[model_providers.${CODEX_PROVIDER_ID}]`,
168
- 'name = "PieceMaker · Proxy PII"',
169
- `base_url = ${tomlString(baseUrl)}`,
170
- 'requires_openai_auth = true',
171
- 'wire_api = "responses"',
172
- 'supports_websockets = false',
173
- CODEX_BLOCK_END,
174
- ];
175
- if (managedStart >= 0 && managedEnd >= managedStart) {
176
- lines.splice(managedStart, managedEnd - managedStart + 1, ...block);
177
- } else {
178
- while (lines.at(-1) === '') lines.pop();
179
- lines.push('', ...block);
180
- }
181
- const normalized = `${lines.join('\n').replace(/\n+$/, '')}\n`;
182
- if (normalized === `${content.replace(/^/, '').replace(/\n*$/, '')}\n`) {
183
- return { configured: true, changed: false, conflict: false, file: configFile };
184
- }
185
- atomicWrite(configFile, normalized);
186
- return { configured: true, changed: true, conflict: false, file: configFile };
106
+ function configureCodexProxy() {
107
+ return { configured: true, changed: false, conflict: false, file: null };
187
108
  }
188
109
 
189
- function bypassCodexProxy({
190
- codexHome = process.env.CODEX_HOME || path.join(os.homedir(), '.codex'),
191
- } = {}) {
192
- const configFile = path.join(codexHome, 'config.toml');
193
- if (!fs.existsSync(configFile)) {
194
- return { bypassed: true, changed: false, conflict: false, file: configFile };
195
- }
196
-
197
- let content;
198
- try { content = fs.readFileSync(configFile, 'utf8'); } catch {
199
- return { bypassed: false, changed: false, conflict: true, file: configFile, reason: 'config-unreadable' };
200
- }
201
-
202
- const lines = content.replace(/^/, '').split(/\r?\n/);
203
- const provider = topLevelAssignment(lines, 'model_provider');
204
- const providerValue = provider ? parseTomlString(provider.raw) : 'openai';
205
- const managedStart = lines.findIndex((line) => line.trim() === CODEX_BLOCK_START);
206
- const managedEnd = lines.findIndex((line) => line.trim() === CODEX_BLOCK_END);
207
- if ((managedStart < 0) !== (managedEnd < 0) || managedEnd < managedStart) {
208
- return { bypassed: false, changed: false, conflict: true, file: configFile, reason: 'provider-block-invalid' };
209
- }
210
- if (providerValue !== CODEX_PROVIDER_ID && managedStart < 0) {
211
- return { bypassed: true, changed: false, conflict: false, file: configFile };
212
- }
213
- if (managedStart >= 0) lines.splice(managedStart, managedEnd - managedStart + 1);
214
-
215
- if (providerValue === CODEX_PROVIDER_ID) {
216
- lines[provider.index] = 'model_provider = "openai"';
217
- if (lines[provider.index - 1]?.trim() === '# Fournisseur Responses protégé par PieceMaker.') {
218
- lines.splice(provider.index - 1, 1);
219
- }
220
- }
221
-
222
- const normalized = `${lines.join('\n').replace(/\n+$/, '')}\n`;
223
- if (normalized === `${content.replace(/^/, '').replace(/\n*$/, '')}\n`) {
224
- return { bypassed: true, changed: false, conflict: false, file: configFile };
225
- }
226
- atomicWrite(configFile, normalized);
227
- return { bypassed: true, changed: true, conflict: false, file: configFile };
110
+ function bypassCodexProxy() {
111
+ return { bypassed: true, changed: false, conflict: false, file: null };
228
112
  }
229
113
 
230
114
  module.exports = {
231
- CODEX_PROVIDER_ID,
232
115
  configureClaudeCodeProxy,
233
116
  bypassClaudeCodeProxy,
234
117
  configureCodexProxy,
@@ -0,0 +1,127 @@
1
+ const { execFileSync, spawn } = require('node:child_process');
2
+ const fs = require('node:fs');
3
+ const net = require('node:net');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+
7
+ const { startRewriterBridge } = require('./rewriter-bridge.cjs');
8
+ const { createSqliteDictionaryLoader } = require('./sqlite-dictionary.cjs');
9
+
10
+ function hudsuckerBinary() {
11
+ if (process.env.PIECEMAKER_HUDSUCKER_BIN) return process.env.PIECEMAKER_HUDSUCKER_BIN;
12
+ return path.join(__dirname, 'hudsucker-proxy', 'target', 'release', 'piecemaker-hudsucker');
13
+ }
14
+
15
+ function makeCertificateAuthority(directory) {
16
+ const cert = path.join(directory, 'ca.crt');
17
+ const key = path.join(directory, 'ca.key');
18
+ execFileSync('openssl', [
19
+ 'req', '-x509', '-newkey', 'rsa:2048', '-nodes',
20
+ '-keyout', key, '-out', cert, '-days', '30',
21
+ '-subj', '/CN=PieceMaker Test CA',
22
+ '-addext', 'basicConstraints=critical,CA:true,pathlen:0',
23
+ '-addext', 'keyUsage=critical,keyCertSign,cRLSign',
24
+ ], { stdio: 'ignore' });
25
+ return { cert, key };
26
+ }
27
+
28
+ function freePort() {
29
+ return new Promise((resolve, reject) => {
30
+ const server = net.createServer();
31
+ server.once('error', reject);
32
+ server.listen(0, '127.0.0.1', () => {
33
+ const { port } = server.address();
34
+ server.close((error) => (error ? reject(error) : resolve(port)));
35
+ });
36
+ });
37
+ }
38
+
39
+ function waitForOutput(child, needle, timeoutMs) {
40
+ return new Promise((resolve, reject) => {
41
+ let buffer = '';
42
+ const timer = setTimeout(() => {
43
+ reject(new Error(`hudsucker n'a pas écouté : ${buffer}`));
44
+ }, timeoutMs);
45
+ const take = (chunk) => {
46
+ buffer += chunk.toString();
47
+ if (!buffer.includes(needle)) return;
48
+ clearTimeout(timer);
49
+ resolve(buffer);
50
+ };
51
+ child.stdout.on('data', take);
52
+ child.stderr.on('data', take);
53
+ child.once('exit', (code) => {
54
+ clearTimeout(timer);
55
+ reject(new Error(`hudsucker sorti (${code}) : ${buffer}`));
56
+ });
57
+ });
58
+ }
59
+
60
+ async function startHudsuckerSession({
61
+ databasePath,
62
+ upstreamPort,
63
+ harness = null,
64
+ hosts = ['api.anthropic.com', 'api.openai.com', 'chatgpt.com'],
65
+ directory = fs.mkdtempSync(path.join(os.tmpdir(), 'pm-hudsucker-')),
66
+ } = {}) {
67
+ const binary = hudsuckerBinary();
68
+ if (!fs.existsSync(binary)) {
69
+ throw new Error(`binaire hudsucker absent : ${binary}`);
70
+ }
71
+ const ca = makeCertificateAuthority(directory);
72
+ const dictionary = createSqliteDictionaryLoader({ databasePath });
73
+ const bridge = await startRewriterBridge({ dictionary, harness });
74
+ const port = await freePort();
75
+ const upstreamMap = hosts.map((host) => `${host}=127.0.0.1:${upstreamPort}`).join(',');
76
+ const child = spawn(binary, [
77
+ '--listen', `127.0.0.1:${port}`,
78
+ '--rewriter', `http://127.0.0.1:${bridge.port}`,
79
+ '--ca-cert', ca.cert,
80
+ '--ca-key', ca.key,
81
+ '--hosts', hosts.join(','),
82
+ '--upstream-map', upstreamMap,
83
+ ], {
84
+ stdio: ['ignore', 'pipe', 'pipe'],
85
+ env: { ...process.env, HTTPS_PROXY: '', HTTP_PROXY: '', ALL_PROXY: '', http_proxy: '', https_proxy: '' },
86
+ });
87
+ try {
88
+ await waitForOutput(child, `listening 127.0.0.1:${port}`, 15000);
89
+ } catch (error) {
90
+ child.kill('SIGKILL');
91
+ await bridge.close();
92
+ dictionary.close();
93
+ throw error;
94
+ }
95
+ return {
96
+ origin: `http://127.0.0.1:${port}`,
97
+ caFile: ca.cert,
98
+ stats: bridge.stats,
99
+ async close() {
100
+ child.kill('SIGTERM');
101
+ await new Promise((resolve) => child.once('exit', resolve));
102
+ await bridge.close();
103
+ dictionary.close();
104
+ },
105
+ };
106
+ }
107
+
108
+ async function postThrough(origin, caFile, body, route = '/v1/messages', host = 'api.anthropic.com') {
109
+ const { ProxyAgent, fetch } = await import('undici');
110
+ const agent = new ProxyAgent({
111
+ uri: origin,
112
+ requestTls: { ca: fs.readFileSync(caFile) },
113
+ });
114
+ const response = await fetch(`https://${host}${route}`, {
115
+ method: 'POST',
116
+ headers: { 'content-type': 'application/json' },
117
+ body,
118
+ dispatcher: agent,
119
+ });
120
+ return response.text();
121
+ }
122
+
123
+ module.exports = {
124
+ hudsuckerBinary,
125
+ postThrough,
126
+ startHudsuckerSession,
127
+ };