@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.
- package/dist/assets/{abnfDiagram-VCTEODGH-C-dTfD4s.js → abnfDiagram-VCTEODGH-C2JDmlZx.js} +1 -1
- package/dist/assets/{arc-BwDrelvt.js → arc-XhSg5Mrj.js} +1 -1
- package/dist/assets/{architectureDiagram-5GKGNRK7-CuViaxCV.js → architectureDiagram-5GKGNRK7-DKREuXxk.js} +1 -1
- package/dist/assets/{blockDiagram-I7D4REHJ-CODUNfgC.js → blockDiagram-I7D4REHJ-CNK0Tp5d.js} +1 -1
- package/dist/assets/{c4Diagram-7LVT6UL2-CiQtyYDd.js → c4Diagram-7LVT6UL2-C8fJzWVp.js} +1 -1
- package/dist/assets/channel-DO5hoFdU.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-DZjjPgAs.js → chunk-2Q5K7J3B-DlE8nLJo.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-C-SPnj5I.js → chunk-5VM5RSS4-CSm-2TZ4.js} +1 -1
- package/dist/assets/{chunk-F27PBJKO-D5b0-fye.js → chunk-F27PBJKO-CSgnXT2c.js} +1 -1
- package/dist/assets/{chunk-IMKFNOWR-CWflUKd3.js → chunk-IMKFNOWR-C6bkt6C1.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-DFltof8Z.js → chunk-JWPE2WC7-jUR52MBO.js} +1 -1
- package/dist/assets/{chunk-POPQ4Y6H-moIw94hw.js → chunk-POPQ4Y6H-D0UAL7O7.js} +1 -1
- package/dist/assets/{chunk-SVP7TREG--H3pige-.js → chunk-SVP7TREG-C9dtOEii.js} +1 -1
- package/dist/assets/{chunk-TICWLB2K-BfujErBt.js → chunk-TICWLB2K-BEzWKT39.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-C5jFgw8p.js → chunk-XXDRQBXY-uD1oklVY.js} +1 -1
- package/dist/assets/classDiagram-ZZMXUADV-BsQtwznI.js +1 -0
- package/dist/assets/classDiagram-v2-VYDZK3BY-BsQtwznI.js +1 -0
- package/dist/assets/{cose-bilkent-JH36ORCC-CsT3oVRI.js → cose-bilkent-JH36ORCC-aGjlSdMe.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-gdpEHNLr.js → cynefin-OW5HDTMX-D3G4OYOn.js} +1 -1
- package/dist/assets/{cynefinDiagram-5FMLGOSQ-v-JmI3Zj.js → cynefinDiagram-5FMLGOSQ-9swRDUnu.js} +1 -1
- package/dist/assets/{dagre-GXQ25YYZ-BvJ_SGF3.js → dagre-GXQ25YYZ-lvaAGdqY.js} +1 -1
- package/dist/assets/{diagram-S7CK7UJ4-B7EHfyrx.js → diagram-S7CK7UJ4-BaZ6YIkR.js} +1 -1
- package/dist/assets/{diagram-UQ7AKVKN-DX5RvUaF.js → diagram-UQ7AKVKN-COJ2Gb38.js} +1 -1
- package/dist/assets/{diagram-VSXAHHWV-D_PJ-E3G.js → diagram-VSXAHHWV-CYZEkbzP.js} +1 -1
- package/dist/assets/{diagram-VX7I27RA-CAtayL03.js → diagram-VX7I27RA-fFeQJ4ae.js} +1 -1
- package/dist/assets/{diagram-Z3DM3KII-BhCCqDLU.js → diagram-Z3DM3KII-B7knszDV.js} +1 -1
- package/dist/assets/{ebnfDiagram-PWID7BFC-BCb926kC.js → ebnfDiagram-PWID7BFC-Dk0l329Y.js} +1 -1
- package/dist/assets/{erDiagram-RLTQ6QDP-5Fw1sblj.js → erDiagram-RLTQ6QDP-CtpYxui8.js} +1 -1
- package/dist/assets/{flowDiagram-HODETNUW-DDUdOLnK.js → flowDiagram-HODETNUW-aVWueRJD.js} +1 -1
- package/dist/assets/{ganttDiagram-EL5Y4UJY-B4IG1idB.js → ganttDiagram-EL5Y4UJY-DfrX-B7p.js} +1 -1
- package/dist/assets/{gitGraphDiagram-WWUBYQGX-S6wvUCsC.js → gitGraphDiagram-WWUBYQGX-BFB1ZPF3.js} +1 -1
- package/dist/assets/{index-CcC0QCGH.js → index-DqRygn9E.js} +170 -169
- package/dist/assets/index-Ds4HaAjz.css +1 -0
- package/dist/assets/{infoDiagram-27XIBGKW-BNv6fEro.js → infoDiagram-27XIBGKW-CuYkzm43.js} +1 -1
- package/dist/assets/{ishikawaDiagram-5VMMS53U-BJZwt-TZ.js → ishikawaDiagram-5VMMS53U-EMiw8Akh.js} +1 -1
- package/dist/assets/{journeyDiagram-3NMN7TZE-BAGhL9Uv.js → journeyDiagram-3NMN7TZE-DCoLV1f8.js} +1 -1
- package/dist/assets/{kanban-definition-UXKFOSKX-DmoLE6ku.js → kanban-definition-UXKFOSKX-CyA4Yinu.js} +1 -1
- package/dist/assets/{layout-ALipSwVQ.js → layout-BlaB0grA.js} +1 -1
- package/dist/assets/{linear-BJkOgP0g.js → linear-DLX3dJDH.js} +1 -1
- package/dist/assets/{mermaid.core-C20iPvcV.js → mermaid.core-Cy51CT89.js} +6 -6
- package/dist/assets/{mindmap-definition-YA3MSWOX-CSI558Pw.js → mindmap-definition-YA3MSWOX-OBXjfL7I.js} +1 -1
- package/dist/assets/{pegDiagram-XKGWAZYB-DFtdseIb.js → pegDiagram-XKGWAZYB-C_BJjUh1.js} +1 -1
- package/dist/assets/{pieDiagram-E7YTZNPT-ByPKZBBy.js → pieDiagram-E7YTZNPT-jIpbr3Fi.js} +1 -1
- package/dist/assets/{quadrantDiagram-AXDQQJYC-tH7z7eR2.js → quadrantDiagram-AXDQQJYC-C1A15xI5.js} +1 -1
- package/dist/assets/{railroadDiagram-O6MQD6OU-D7ZdvS0P.js → railroadDiagram-O6MQD6OU-Rslzn1pI.js} +1 -1
- package/dist/assets/{requirementDiagram-BXWQKSXE-nSTqp6wv.js → requirementDiagram-BXWQKSXE-LlfURO5-.js} +1 -1
- package/dist/assets/{sankeyDiagram-P5KCCOFB-Bg3ivRCq.js → sankeyDiagram-P5KCCOFB-B5BPF4-O.js} +1 -1
- package/dist/assets/{sequenceDiagram-WJ2MYXX4-BFs4BVKR.js → sequenceDiagram-WJ2MYXX4-orYZe9zX.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-DuSN3zyt.js → sizeCapture-INFHLROL-Co-K5Cfb.js} +1 -1
- package/dist/assets/{stateDiagram-D77RDMKH---hK4wFR.js → stateDiagram-D77RDMKH-COEx1aH2.js} +1 -1
- package/dist/assets/{stateDiagram-v2-MP3YSRHH-DxBEemHS.js → stateDiagram-v2-MP3YSRHH-DaeTZvmy.js} +1 -1
- package/dist/assets/{swimlanes-42K2YHIH-C6RXO1Rv.js → swimlanes-42K2YHIH-BHQaDhfZ.js} +1 -1
- package/dist/assets/swimlanesDiagram-VR7AAH4N-BHW8q5M5.js +8 -0
- package/dist/assets/{timeline-definition-24CTP7MA-Ci0Toxgn.js → timeline-definition-24CTP7MA-COqCRwGX.js} +1 -1
- package/dist/assets/{vennDiagram-4TSXK5OY-CKF_VmKX.js → vennDiagram-4TSXK5OY-nym5XSnr.js} +1 -1
- package/dist/assets/{wardleyDiagram-VM6X3IG4-Xao4cR_d.js → wardleyDiagram-VM6X3IG4-DEPPey_W.js} +1 -1
- package/dist/assets/{xychartDiagram-S5SC5T6Z-BAZUt04h.js → xychartDiagram-S5SC5T6Z-uSJ404kL.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/sw.js +1 -1
- package/dist-server/server/index.js +3 -1
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/modules/cli/desktop-shortcut.js +80 -0
- package/dist-server/server/modules/cli/desktop-shortcut.js.map +1 -0
- package/dist-server/server/modules/cli/pwa-launcher.js +27 -4
- package/dist-server/server/modules/cli/pwa-launcher.js.map +1 -1
- package/dist-server/server/piecemaker/index.js +7 -3
- package/dist-server/server/piecemaker/index.js.map +1 -1
- package/dist-server/server/piecemaker/knowledge/index.js +13 -6
- package/dist-server/server/piecemaker/knowledge/index.js.map +1 -1
- package/dist-server/server/piecemaker/knowledge/local-routes.js +55 -0
- package/dist-server/server/piecemaker/knowledge/local-routes.js.map +1 -0
- package/dist-server/server/piecemaker/knowledge/scan-jobs.js +3 -1
- package/dist-server/server/piecemaker/knowledge/scan-jobs.js.map +1 -1
- package/dist-server/server/piecemaker/library/index.js +14 -41
- package/dist-server/server/piecemaker/library/index.js.map +1 -1
- package/dist-server/server/piecemaker/library/store.js +11 -3
- package/dist-server/server/piecemaker/library/store.js.map +1 -1
- package/dist-server/server/piecemaker/library/workspace-installation.js +26 -3
- package/dist-server/server/piecemaker/library/workspace-installation.js.map +1 -1
- package/dist-server/server/piecemaker/timesheet/service.js +8 -7
- package/dist-server/server/piecemaker/timesheet/service.js.map +1 -1
- package/dist-server/server/piecemaker/timesheet/store.js +5 -3
- package/dist-server/server/piecemaker/timesheet/store.js.map +1 -1
- package/package.json +1 -1
- package/product.config.json +3 -0
- package/scripts/piecemaker/build-app-icon.py +88 -0
- package/scripts/piecemaker/cli/lib/app-window.mjs +47 -0
- package/scripts/piecemaker/cli/lib/piecemaker-anonymizer.mjs +2 -2
- package/scripts/piecemaker/cli/lib/pwa.mjs +10 -13
- package/scripts/piecemaker/cli/piecemaker.mjs +15 -1
- package/scripts/piecemaker/hooks/proxy-guard.mjs +19 -54
- package/scripts/release/assert-piecemaker-version.mjs +25 -0
- package/server/index.ts +4 -1
- package/server/modules/cli/desktop-shortcut.ts +89 -0
- package/server/modules/cli/pwa-launcher.ts +29 -4
- package/server/piecemaker/anonymizer/client-config.cjs +45 -162
- package/server/piecemaker/anonymizer/hudsucker-fixture.cjs +127 -0
- package/server/piecemaker/anonymizer/hudsucker-proxy/Cargo.lock +2396 -0
- package/server/piecemaker/anonymizer/hudsucker-proxy/Cargo.toml +14 -0
- package/server/piecemaker/anonymizer/hudsucker-proxy/src/main.rs +501 -0
- package/server/piecemaker/anonymizer/providers.cjs +14 -135
- package/server/piecemaker/anonymizer/rewriter-bridge.cjs +221 -0
- package/server/piecemaker/anonymizer/service.cjs +144 -28
- package/server/piecemaker/index.ts +8 -3
- package/server/piecemaker/knowledge/index.ts +14 -6
- package/server/piecemaker/knowledge/local-routes.ts +72 -0
- package/server/piecemaker/knowledge/scan-jobs.ts +4 -1
- package/server/piecemaker/library/index.ts +14 -34
- package/server/piecemaker/library/store.ts +11 -3
- package/server/piecemaker/library/workspace-installation.ts +20 -3
- package/server/piecemaker/timesheet/service.ts +9 -7
- package/server/piecemaker/timesheet/store.ts +5 -3
- package/server/piecemaker/vendor/installer/assets/claude-hooks/piecemaker-guard-secrets.mjs +0 -1
- package/server/piecemaker/vendor/installer/assets/piecemaker.icns +0 -0
- package/server/piecemaker/vendor/installer/assets/piecemaker.ico +0 -0
- package/server/piecemaker/vendor/installer/bin/piecemaker.mjs +56 -206
- package/server/piecemaker/vendor/installer/lib/commandes.mjs +2 -4
- package/server/piecemaker/vendor/installer/lib/conversion-client.mjs +74 -0
- package/server/piecemaker/vendor/installer/lib/service.mjs +4 -358
- package/server/piecemaker/vendor/installer/lib/state.mjs +0 -6
- package/server/piecemaker/vendor/installer/steps/03-python-gliner.mjs +4 -4
- package/server/piecemaker/vendor/installer/steps/09-claude-assets.mjs +0 -31
- package/server/piecemaker/vendor/piecemaker-plugin/scripts/statusline.mjs +3 -4
- package/server/piecemaker/vendor/websocket-server/admin-routes.cjs +1 -49
- package/server/piecemaker/vendor/websocket-server/claude-hooks.cjs +14 -12
- package/server/piecemaker/vendor/websocket-server/originals-pipeline.cjs +17 -476
- package/server/piecemaker/vendor/websocket-server/scripts/convert_and_scan_pipeline.py +2 -2
- package/server/piecemaker/vendor/websocket-server/scripts/mineru_improved_1.py +11 -2
- package/server/piecemaker/vendor/websocket-server/scripts/presidio-gliner/scanner_worker.py +13 -37
- package/server/piecemaker/vendor/websocket-server/scripts/requirements.txt +1 -7
- package/server/piecemaker/vendor/websocket-server/scripts/warmup.py +14 -85
- package/dist/assets/channel-DIjrikYC.js +0 -1
- package/dist/assets/classDiagram-ZZMXUADV-Yagpznfi.js +0 -1
- package/dist/assets/classDiagram-v2-VYDZK3BY-Yagpznfi.js +0 -1
- package/dist/assets/index-D_Dc_nQv.css +0 -1
- package/dist/assets/swimlanesDiagram-VR7AAH4N-BC3fwmoQ.js +0 -8
- package/server/piecemaker/anonymizer/proxy.cjs +0 -365
- package/server/piecemaker/vendor/installer/assets/PieceMakerApp.swift +0 -189
- package/server/piecemaker/vendor/installer/lib/desktop-launcher.mjs +0 -305
- package/server/piecemaker/vendor/installer/steps/05-certificats.mjs +0 -171
- package/server/piecemaker/vendor/installer/steps/14-mxc-sandbox.mjs +0 -271
- package/server/piecemaker/vendor/installer/steps/15-pwa-desktop.mjs +0 -64
- package/server/piecemaker/vendor/installer/templates/root-CLAUDE.md +0 -56
- 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
|
-
|
|
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.
|
|
130
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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({
|
|
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
|
|
54
|
-
if (
|
|
55
|
-
return { configured: false, changed: false, conflict: true, file: settingsFile, reason: '
|
|
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 (
|
|
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 (!
|
|
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
|
-
|
|
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
|
|
97
|
-
return
|
|
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
|
-
|
|
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
|
+
};
|