@parall/cli 1.55.3 → 1.55.5
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/commands/profile.d.ts +3 -0
- package/dist/commands/profile.d.ts.map +1 -0
- package/dist/commands/profile.js +114 -0
- package/dist/commands/projects.d.ts.map +1 -1
- package/dist/commands/projects.js +132 -2
- package/dist/commands/refs.d.ts +3 -2
- package/dist/commands/refs.d.ts.map +1 -1
- package/dist/commands/refs.js +12 -6
- package/dist/commands/tasks.d.ts.map +1 -1
- package/dist/commands/tasks.js +12 -3
- package/dist/commands/teams.d.ts +3 -0
- package/dist/commands/teams.d.ts.map +1 -0
- package/dist/commands/teams.js +102 -0
- package/dist/commands/wechat.d.ts +4 -3
- package/dist/commands/wechat.d.ts.map +1 -1
- package/dist/commands/wechat.js +31 -3
- package/dist/index.js +11 -0
- package/dist/lib/client.d.ts +24 -6
- package/dist/lib/client.d.ts.map +1 -1
- package/dist/lib/client.js +77 -35
- package/dist/lib/output.d.ts +1 -1
- package/dist/lib/output.d.ts.map +1 -1
- package/dist/lib/output.js +8 -2
- package/dist/lib/update-check.d.ts +29 -0
- package/dist/lib/update-check.d.ts.map +1 -0
- package/dist/lib/update-check.js +269 -0
- package/dist/lib/upload.d.ts.map +1 -1
- package/dist/lib/upload.js +64 -9
- package/dist/lib/wiki.d.ts +9 -3
- package/dist/lib/wiki.d.ts.map +1 -1
- package/dist/lib/wiki.js +15 -8
- package/dist/update-check-worker.d.ts +3 -0
- package/dist/update-check-worker.d.ts.map +1 -0
- package/dist/update-check-worker.js +5 -0
- package/package.json +4 -4
package/dist/lib/client.d.ts
CHANGED
|
@@ -28,6 +28,13 @@ export type RuntimeContext = {
|
|
|
28
28
|
dispatchSourceId?: string;
|
|
29
29
|
/** Trigger thread root (parel invocation context) — the by-source send's default thread. */
|
|
30
30
|
threadRootId?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Conversation generation of this execution (parel v2 invocation context,
|
|
33
|
+
* PRLL_GENERATION). Rides every dispatch_source send; the server rejects a
|
|
34
|
+
* mutation whose generation predates a New Session barrier. Absent on v1
|
|
35
|
+
* and lane-bound turns.
|
|
36
|
+
*/
|
|
37
|
+
generation?: number;
|
|
31
38
|
};
|
|
32
39
|
/**
|
|
33
40
|
* Read per-dispatch context from ENV + sideband files.
|
|
@@ -49,25 +56,36 @@ export declare function resolveRuntimeContext(): RuntimeContext;
|
|
|
49
56
|
export declare function resolveSourceDispatchBinding(ctx: RuntimeContext, chatId: string, threadRootId: string | undefined): {
|
|
50
57
|
source_type: string;
|
|
51
58
|
source_id: string;
|
|
59
|
+
generation?: number;
|
|
52
60
|
} | null;
|
|
53
61
|
/** Typed dispatch binding for a task write, derived from the turn context. */
|
|
54
62
|
export type TypedDispatchBinding = {
|
|
55
|
-
lane
|
|
63
|
+
/** Claimed typed lane (v1 consumers). Absent on the converged transport,
|
|
64
|
+
* where no lane exists and the write binds by dispatch_event_id under the
|
|
65
|
+
* generation fence instead. */
|
|
66
|
+
lane?: string;
|
|
56
67
|
dispatchEventId: string;
|
|
68
|
+
/** Conversation generation (converged transport, PRLL_GENERATION). */
|
|
69
|
+
generation?: number;
|
|
57
70
|
/** Set for the first (resolving) update of this dispatch. */
|
|
58
71
|
effectKey?: string;
|
|
59
72
|
markCommitted: () => void;
|
|
60
73
|
};
|
|
61
74
|
/**
|
|
62
|
-
* Resolve the typed
|
|
63
|
-
* current turn must be a typed dispatch
|
|
64
|
-
*
|
|
75
|
+
* Resolve the typed dispatch binding for `parall task update <taskId>`: the
|
|
76
|
+
* current turn must be a typed dispatch about this exact task.
|
|
77
|
+
*
|
|
78
|
+
* Lane-bound (v1): the first update carries the canonical
|
|
65
79
|
* `task_update:<dispatch_event_id>` effect key (resolves the WorkItem in the
|
|
66
|
-
* same transaction); later updates go keyless (lane-checked only)
|
|
67
|
-
* committed flag
|
|
80
|
+
* same transaction); later updates go keyless (lane-checked only), with the
|
|
81
|
+
* committed flag in a CLI-owned sidecar keyed by the typed lane. When
|
|
68
82
|
* PRLL_CONTEXT_DIR is absent the sidecar can't exist, so no effect key is
|
|
69
83
|
* ever attached (a blind key would make a *different* second update replay
|
|
70
84
|
* the first one's result instead of applying).
|
|
85
|
+
*
|
|
86
|
+
* Converged (no lane): the caller appends a payload hash to the base key —
|
|
87
|
+
* content addressing gives every distinct update its own idempotent key, so
|
|
88
|
+
* no sidecar state is needed at all.
|
|
71
89
|
*/
|
|
72
90
|
export declare function resolveTypedDispatchBinding(taskId: string): TypedDispatchBinding | null;
|
|
73
91
|
/** Per-lane dispatch context (PRLL_CONTEXT_DIR contract), keyed by send target. */
|
package/dist/lib/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/lib/client.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,MAAM,mBAAmB,GAAG;IAChC,MAAM,EAAE,YAAY,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,6DAA6D;AAC7D,MAAM,MAAM,cAAc,GAAG;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,oGAAoG;IACpG,OAAO,EAAE,OAAO,CAAC;IACjB,8EAA8E;IAC9E,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,sFAAsF;IACtF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,4FAA4F;IAC5F,YAAY,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/lib/client.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,MAAM,mBAAmB,GAAG;IAChC,MAAM,EAAE,YAAY,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,6DAA6D;AAC7D,MAAM,MAAM,cAAc,GAAG;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,oGAAoG;IACpG,OAAO,EAAE,OAAO,CAAC;IACjB,8EAA8E;IAC9E,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,sFAAsF;IACtF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,4FAA4F;IAC5F,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,IAAI,cAAc,CAkEtD;AAED;;;;;;;GAOG;AACH,wBAAgB,4BAA4B,CAC1C,GAAG,EAAE,cAAc,EACnB,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,GAAG,SAAS,GAC/B;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAWxE;AAED,8EAA8E;AAC9E,MAAM,MAAM,oBAAoB,GAAG;IACjC;;oCAEgC;IAChC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,eAAe,EAAE,MAAM,CAAC;IACxB,sEAAsE;IACtE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,6DAA6D;IAC7D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,IAAI,CAAC;CAC3B,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM,GAAG,oBAAoB,GAAG,IAAI,CA8DvF;AAED,mFAAmF;AACnF,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,cAAc,EAAE,OAAO,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF;;;;GAIG;AACH,qBAAa,gBAAiB,SAAQ,KAAK;CAAG;AAE9C;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,MAAM,EACd,YAAY,CAAC,EAAE,MAAM,GACpB,mBAAmB,GAAG,IAAI,CA+E5B;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,mBAAmB,GAAG,IAAI,CAYzE;AAED,wBAAgB,kBAAkB,IAAI,mBAAmB,CA8BxD"}
|
package/dist/lib/client.js
CHANGED
|
@@ -18,10 +18,15 @@ export function resolveRuntimeContext() {
|
|
|
18
18
|
const dispatchSourceType = process.env.PRLL_DISPATCH_SOURCE_TYPE?.trim() || undefined;
|
|
19
19
|
const dispatchSourceId = process.env.PRLL_DISPATCH_SOURCE_ID?.trim() || undefined;
|
|
20
20
|
const threadRootId = process.env.PRLL_THREAD_ROOT_ID?.trim() || undefined;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
const generationRaw = process.env.PRLL_GENERATION?.trim();
|
|
22
|
+
const generation = generationRaw && /^\d+$/.test(generationRaw) ? Number(generationRaw) : undefined;
|
|
23
|
+
// Parel v2 surfaces the lane context as plain env (there is no context
|
|
24
|
+
// file inside a parel sandbox); the CC/Codex context file below still wins
|
|
25
|
+
// per-field when both exist.
|
|
26
|
+
let dispatchEventId = process.env.PRLL_DISPATCH_EVENT_ID?.trim() || undefined;
|
|
27
|
+
let lane = process.env.PRLL_DISPATCH_LANE?.trim() || undefined;
|
|
28
|
+
let laneTargetUri = process.env.PRLL_LANE_TARGET_URI?.trim() || undefined;
|
|
29
|
+
let taskId = process.env.PRLL_TASK_ID?.trim() || undefined;
|
|
25
30
|
if (process.env.PRLL_CONTEXT_FILE) {
|
|
26
31
|
try {
|
|
27
32
|
const raw = fs.readFileSync(process.env.PRLL_CONTEXT_FILE, 'utf-8').trim();
|
|
@@ -33,10 +38,12 @@ export function resolveRuntimeContext() {
|
|
|
33
38
|
stepId ??= ctx.step_id || undefined;
|
|
34
39
|
if (ctx.no_reply === true)
|
|
35
40
|
noReply = true;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
// The file wins per-field over the parel env fallbacks above, but an
|
|
42
|
+
// absent field must not erase them.
|
|
43
|
+
dispatchEventId = ctx.dispatch_event_id || dispatchEventId;
|
|
44
|
+
lane = ctx.lane || lane;
|
|
45
|
+
laneTargetUri = ctx.target_uri || laneTargetUri;
|
|
46
|
+
taskId = ctx.task_id || taskId;
|
|
40
47
|
}
|
|
41
48
|
}
|
|
42
49
|
catch {
|
|
@@ -66,6 +73,7 @@ export function resolveRuntimeContext() {
|
|
|
66
73
|
dispatchSourceType,
|
|
67
74
|
dispatchSourceId,
|
|
68
75
|
threadRootId,
|
|
76
|
+
generation,
|
|
69
77
|
};
|
|
70
78
|
}
|
|
71
79
|
/**
|
|
@@ -83,36 +91,67 @@ export function resolveSourceDispatchBinding(ctx, chatId, threadRootId) {
|
|
|
83
91
|
return null;
|
|
84
92
|
if ((threadRootId ?? '') !== (ctx.threadRootId ?? ''))
|
|
85
93
|
return null;
|
|
86
|
-
return {
|
|
94
|
+
return {
|
|
95
|
+
source_type: ctx.dispatchSourceType,
|
|
96
|
+
source_id: ctx.dispatchSourceId,
|
|
97
|
+
// v2 executions carry their conversation generation; the server fences
|
|
98
|
+
// stale-generation replies inside the commit transaction.
|
|
99
|
+
...(ctx.generation !== undefined ? { generation: ctx.generation } : {}),
|
|
100
|
+
};
|
|
87
101
|
}
|
|
88
102
|
/**
|
|
89
|
-
* Resolve the typed
|
|
90
|
-
* current turn must be a typed dispatch
|
|
91
|
-
*
|
|
103
|
+
* Resolve the typed dispatch binding for `parall task update <taskId>`: the
|
|
104
|
+
* current turn must be a typed dispatch about this exact task.
|
|
105
|
+
*
|
|
106
|
+
* Lane-bound (v1): the first update carries the canonical
|
|
92
107
|
* `task_update:<dispatch_event_id>` effect key (resolves the WorkItem in the
|
|
93
|
-
* same transaction); later updates go keyless (lane-checked only)
|
|
94
|
-
* committed flag
|
|
108
|
+
* same transaction); later updates go keyless (lane-checked only), with the
|
|
109
|
+
* committed flag in a CLI-owned sidecar keyed by the typed lane. When
|
|
95
110
|
* PRLL_CONTEXT_DIR is absent the sidecar can't exist, so no effect key is
|
|
96
111
|
* ever attached (a blind key would make a *different* second update replay
|
|
97
112
|
* the first one's result instead of applying).
|
|
113
|
+
*
|
|
114
|
+
* Converged (no lane): the caller appends a payload hash to the base key —
|
|
115
|
+
* content addressing gives every distinct update its own idempotent key, so
|
|
116
|
+
* no sidecar state is needed at all.
|
|
98
117
|
*/
|
|
99
118
|
export function resolveTypedDispatchBinding(taskId) {
|
|
100
119
|
const ctx = resolveRuntimeContext();
|
|
101
|
-
if (!ctx.
|
|
102
|
-
return null;
|
|
103
|
-
if (!ctx.laneTargetUri?.startsWith('dsp:'))
|
|
120
|
+
if (!ctx.dispatchEventId)
|
|
104
121
|
return null;
|
|
105
122
|
if (!ctx.taskId || ctx.taskId !== taskId)
|
|
106
123
|
return null;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
124
|
+
let binding;
|
|
125
|
+
let sidecarKey;
|
|
126
|
+
if (!ctx.lane) {
|
|
127
|
+
// Converged transport: no lane exists — the write binds by
|
|
128
|
+
// dispatch_event_id under the server's generation fence. A context with
|
|
129
|
+
// no generation must NOT produce a binding: an unfenced by-id write
|
|
130
|
+
// would bypass the New Session barrier (the server rejects it anyway —
|
|
131
|
+
// failing closed here keeps the task update itself working as a plain
|
|
132
|
+
// write). Content-addressed keys replace the reply-slot sidecar on this
|
|
133
|
+
// path: the caller appends a payload hash to the base key, so a
|
|
134
|
+
// same-payload retry replays its own key idempotently while a different
|
|
135
|
+
// second update mints a new key and lands as its own Effect.
|
|
136
|
+
if (ctx.generation === undefined)
|
|
137
|
+
return null;
|
|
138
|
+
return {
|
|
139
|
+
dispatchEventId: ctx.dispatchEventId,
|
|
140
|
+
generation: ctx.generation,
|
|
141
|
+
effectKey: `task_update:${ctx.dispatchEventId}`,
|
|
142
|
+
markCommitted: () => { },
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
// Lane-bound (v1 consumers): the write rides the claimed typed lane and
|
|
146
|
+
// the reply-slot sidecar arbitrates the single resolving key.
|
|
147
|
+
if (!ctx.laneTargetUri?.startsWith('dsp:'))
|
|
148
|
+
return null;
|
|
149
|
+
binding = { lane: ctx.lane, dispatchEventId: ctx.dispatchEventId, markCommitted: () => { } };
|
|
150
|
+
sidecarKey = ctx.laneTargetUri;
|
|
112
151
|
const contextDir = process.env.PRLL_CONTEXT_DIR?.trim();
|
|
113
152
|
if (!contextDir)
|
|
114
153
|
return binding;
|
|
115
|
-
const sidecarPath = laneReplyStateFilePath(contextDir,
|
|
154
|
+
const sidecarPath = laneReplyStateFilePath(contextDir, sidecarKey);
|
|
116
155
|
let committed = false;
|
|
117
156
|
try {
|
|
118
157
|
const sidecar = JSON.parse(fs.readFileSync(sidecarPath, 'utf-8'));
|
|
@@ -122,19 +161,22 @@ export function resolveTypedDispatchBinding(taskId) {
|
|
|
122
161
|
catch {
|
|
123
162
|
// No sidecar yet — resolving update not committed.
|
|
124
163
|
}
|
|
125
|
-
if (
|
|
126
|
-
|
|
127
|
-
binding.effectKey
|
|
128
|
-
binding
|
|
129
|
-
try {
|
|
130
|
-
fs.writeFileSync(sidecarPath, JSON.stringify({ dispatch_event_id: dispatchEventId, reply_committed: true }), 'utf-8');
|
|
131
|
-
}
|
|
132
|
-
catch {
|
|
133
|
-
// Best-effort — a lost sidecar means the next update retries the key
|
|
134
|
-
// and the server's effect ledger replies idempotently.
|
|
135
|
-
}
|
|
136
|
-
};
|
|
164
|
+
if (committed) {
|
|
165
|
+
// Resolving update already landed: later updates go keyless.
|
|
166
|
+
delete binding.effectKey;
|
|
167
|
+
return binding;
|
|
137
168
|
}
|
|
169
|
+
const dispatchEventId = ctx.dispatchEventId;
|
|
170
|
+
binding.effectKey = `task_update:${dispatchEventId}`;
|
|
171
|
+
binding.markCommitted = () => {
|
|
172
|
+
try {
|
|
173
|
+
fs.writeFileSync(sidecarPath, JSON.stringify({ dispatch_event_id: dispatchEventId, reply_committed: true }), 'utf-8');
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
// Best-effort — a lost sidecar means the next update retries the key
|
|
177
|
+
// and the server's effect ledger replies idempotently.
|
|
178
|
+
}
|
|
179
|
+
};
|
|
138
180
|
return binding;
|
|
139
181
|
}
|
|
140
182
|
/**
|
package/dist/lib/output.d.ts
CHANGED
|
@@ -21,5 +21,5 @@ export declare function ensurePrllScheme(idOrUri: string): string;
|
|
|
21
21
|
* than receiving a malformed value.
|
|
22
22
|
*/
|
|
23
23
|
export declare function parsePositiveInt(raw: string | undefined): number | undefined;
|
|
24
|
-
export declare function printError(err: unknown): never;
|
|
24
|
+
export declare function printError(err: unknown, attemptedPayload?: Record<string, unknown>): never;
|
|
25
25
|
//# sourceMappingURL=output.d.ts.map
|
package/dist/lib/output.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../../src/lib/output.ts"],"names":[],"mappings":"AAEA,wBAAgB,SAAS,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAE7C;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,SAAY,GAAG,IAAI,CAEvE;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAExD;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAW5E;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,OAAO,GAAG,KAAK,
|
|
1
|
+
{"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../../src/lib/output.ts"],"names":[],"mappings":"AAEA,wBAAgB,SAAS,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAE7C;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,SAAY,GAAG,IAAI,CAEvE;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAExD;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAW5E;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,OAAO,EAAE,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,KAAK,CA8C1F"}
|
package/dist/lib/output.js
CHANGED
|
@@ -43,7 +43,7 @@ export function parsePositiveInt(raw) {
|
|
|
43
43
|
const n = Number(t);
|
|
44
44
|
return Number.isInteger(n) && n > 0 ? n : undefined;
|
|
45
45
|
}
|
|
46
|
-
export function printError(err) {
|
|
46
|
+
export function printError(err, attemptedPayload) {
|
|
47
47
|
if (err instanceof ApiError) {
|
|
48
48
|
// Faithful, parseable line: the server's real message + machine anchors.
|
|
49
49
|
console.error(JSON.stringify({
|
|
@@ -70,7 +70,13 @@ export function printError(err) {
|
|
|
70
70
|
? 'This action can be approved.'
|
|
71
71
|
: 'This action may be approvable.';
|
|
72
72
|
const chat = err.resourceUri?.startsWith('prll://cht_') ? err.resourceUri : '<chat_id>';
|
|
73
|
-
|
|
73
|
+
// When the failing command knows exactly what it tried to write, the
|
|
74
|
+
// proposal command is prefilled with that payload — the deny → propose
|
|
75
|
+
// loop then needs no re-assembly by the caller.
|
|
76
|
+
const payloadArg = attemptedPayload
|
|
77
|
+
? ` --payload '${JSON.stringify(attemptedPayload).replaceAll("'", `'\\''`)}'`
|
|
78
|
+
: '';
|
|
79
|
+
console.error(`${lead} Request approval:\n parall approvals request --action ${err.action} --resource ${err.resourceUri ?? '<resource>'}${payloadArg} --chat ${chat} --title "..." --reason "..."`);
|
|
74
80
|
}
|
|
75
81
|
}
|
|
76
82
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
export declare const UPDATE_CHECK_TIMEOUT_MS = 1500;
|
|
3
|
+
type UpdateNotifierOptions = {
|
|
4
|
+
now?: () => number;
|
|
5
|
+
cachePath?: string;
|
|
6
|
+
spawnWorker?: (cachePath: string) => void;
|
|
7
|
+
writeHint?: (hint: string) => void;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
};
|
|
10
|
+
type RefreshOptions = {
|
|
11
|
+
now?: () => number;
|
|
12
|
+
fetch?: typeof fetch;
|
|
13
|
+
timeoutMs?: number;
|
|
14
|
+
registryUrl?: string;
|
|
15
|
+
};
|
|
16
|
+
export type UpdateNotifier = {
|
|
17
|
+
afterSuccessfulCommand: () => void;
|
|
18
|
+
};
|
|
19
|
+
export declare function isNewerRelease(currentVersion: string, latestVersion: string): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Acquire the cache's cross-process lock. The token check prevents a stale
|
|
22
|
+
* owner from deleting a replacement lock when it eventually resumes.
|
|
23
|
+
*/
|
|
24
|
+
export declare function acquireUpdateStateLock(cachePath: string, now?: () => number): (() => void) | null;
|
|
25
|
+
export declare function spawnUpdateCheckWorker(cachePath: string, spawnImpl?: typeof spawn): void;
|
|
26
|
+
export declare function createUpdateNotifier(currentVersion: string, options?: UpdateNotifierOptions): UpdateNotifier;
|
|
27
|
+
export declare function refreshUpdateCache(cachePath: string, options?: RefreshOptions): Promise<void>;
|
|
28
|
+
export {};
|
|
29
|
+
//# sourceMappingURL=update-check.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"update-check.d.ts","sourceRoot":"","sources":["../../src/lib/update-check.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAW3C,eAAO,MAAM,uBAAuB,OAAQ,CAAC;AAS7C,KAAK,qBAAqB,GAAG;IAC3B,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF,KAAK,cAAc,GAAG;IACpB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,sBAAsB,EAAE,MAAM,IAAI,CAAC;CACpC,CAAC;AAQF,wBAAgB,cAAc,CAAC,cAAc,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAUrF;AA4CD;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,MAAM,EACjB,GAAG,GAAE,MAAM,MAAiB,GAC3B,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAkErB;AAgBD,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,GAAE,OAAO,KAAa,GAAG,IAAI,CAW/F;AAWD,wBAAgB,oBAAoB,CAClC,cAAc,EAAE,MAAM,EACtB,OAAO,GAAE,qBAA0B,GAClC,cAAc,CAyDhB;AAED,wBAAsB,kBAAkB,CACtC,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CA+Bf"}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import * as os from 'node:os';
|
|
5
|
+
import * as path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
const REGISTRY_URL = 'https://registry.npmjs.org/@parall%2Fcli/latest';
|
|
8
|
+
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
9
|
+
const RETRY_INTERVAL_MS = 60 * 60 * 1000;
|
|
10
|
+
const LOCK_STALE_MS = 30_000;
|
|
11
|
+
export const UPDATE_CHECK_TIMEOUT_MS = 1_500;
|
|
12
|
+
function releaseParts(version) {
|
|
13
|
+
const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.exec(version);
|
|
14
|
+
if (!match)
|
|
15
|
+
return null;
|
|
16
|
+
return [BigInt(match[1]), BigInt(match[2]), BigInt(match[3])];
|
|
17
|
+
}
|
|
18
|
+
export function isNewerRelease(currentVersion, latestVersion) {
|
|
19
|
+
const current = releaseParts(currentVersion);
|
|
20
|
+
const latest = releaseParts(latestVersion);
|
|
21
|
+
if (!current || !latest)
|
|
22
|
+
return false;
|
|
23
|
+
for (let i = 0; i < current.length; i++) {
|
|
24
|
+
if (latest[i] > current[i])
|
|
25
|
+
return true;
|
|
26
|
+
if (latest[i] < current[i])
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
function defaultCachePath() {
|
|
32
|
+
const envRoot = process.env.XDG_CACHE_HOME?.trim();
|
|
33
|
+
const cacheRoot = envRoot ||
|
|
34
|
+
(process.platform === 'win32' && process.env.LOCALAPPDATA?.trim()) ||
|
|
35
|
+
path.join(os.homedir(), '.cache');
|
|
36
|
+
return path.join(cacheRoot, 'parall', 'update-check.json');
|
|
37
|
+
}
|
|
38
|
+
function readState(cachePath) {
|
|
39
|
+
try {
|
|
40
|
+
const parsed = JSON.parse(fs.readFileSync(cachePath, 'utf8'));
|
|
41
|
+
const state = {};
|
|
42
|
+
if (typeof parsed.latest_version === 'string')
|
|
43
|
+
state.latest_version = parsed.latest_version;
|
|
44
|
+
if (typeof parsed.last_success_at === 'number')
|
|
45
|
+
state.last_success_at = parsed.last_success_at;
|
|
46
|
+
if (typeof parsed.last_attempt_at === 'number')
|
|
47
|
+
state.last_attempt_at = parsed.last_attempt_at;
|
|
48
|
+
if (typeof parsed.last_notified_at === 'number') {
|
|
49
|
+
state.last_notified_at = parsed.last_notified_at;
|
|
50
|
+
}
|
|
51
|
+
return state;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function writeState(cachePath, state) {
|
|
58
|
+
const tempPath = `${cachePath}.${process.pid}.tmp`;
|
|
59
|
+
try {
|
|
60
|
+
fs.mkdirSync(path.dirname(cachePath), { recursive: true, mode: 0o700 });
|
|
61
|
+
fs.writeFileSync(tempPath, `${JSON.stringify(state)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
62
|
+
fs.renameSync(tempPath, cachePath);
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
try {
|
|
67
|
+
fs.unlinkSync(tempPath);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// Best-effort cleanup only.
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Acquire the cache's cross-process lock. The token check prevents a stale
|
|
77
|
+
* owner from deleting a replacement lock when it eventually resumes.
|
|
78
|
+
*/
|
|
79
|
+
export function acquireUpdateStateLock(cachePath, now = Date.now) {
|
|
80
|
+
const lockPath = `${cachePath}.lock`;
|
|
81
|
+
const withTokenGate = (token, action) => {
|
|
82
|
+
const tokenHash = createHash('sha256').update(token).digest('hex');
|
|
83
|
+
const gatePath = `${lockPath}.reclaim-${tokenHash}`;
|
|
84
|
+
const gateToken = `${process.pid}:${randomUUID()}`;
|
|
85
|
+
try {
|
|
86
|
+
fs.writeFileSync(gatePath, gateToken, { encoding: 'utf8', flag: 'wx', mode: 0o600 });
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
return action();
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
try {
|
|
96
|
+
if (fs.readFileSync(gatePath, 'utf8') === gateToken)
|
|
97
|
+
fs.unlinkSync(gatePath);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// A lost/replaced gate is already released from this owner's perspective.
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const tryAcquire = () => {
|
|
105
|
+
const token = `${process.pid}:${randomUUID()}`;
|
|
106
|
+
try {
|
|
107
|
+
fs.mkdirSync(path.dirname(cachePath), { recursive: true, mode: 0o700 });
|
|
108
|
+
fs.writeFileSync(lockPath, token, { encoding: 'utf8', flag: 'wx', mode: 0o600 });
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
return () => {
|
|
114
|
+
withTokenGate(token, () => {
|
|
115
|
+
try {
|
|
116
|
+
if (fs.readFileSync(lockPath, 'utf8') === token)
|
|
117
|
+
fs.unlinkSync(lockPath);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// A lost/replaced lock is already released from this owner's perspective.
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
const release = tryAcquire();
|
|
126
|
+
if (release)
|
|
127
|
+
return release;
|
|
128
|
+
let staleToken;
|
|
129
|
+
try {
|
|
130
|
+
staleToken = fs.readFileSync(lockPath, 'utf8');
|
|
131
|
+
if (now() - fs.statSync(lockPath).mtimeMs < LOCK_STALE_MS)
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
return err.code === 'ENOENT' ? tryAcquire() : null;
|
|
136
|
+
}
|
|
137
|
+
return (withTokenGate(staleToken, () => {
|
|
138
|
+
try {
|
|
139
|
+
if (fs.readFileSync(lockPath, 'utf8') !== staleToken)
|
|
140
|
+
return null;
|
|
141
|
+
if (now() - fs.statSync(lockPath).mtimeMs < LOCK_STALE_MS)
|
|
142
|
+
return null;
|
|
143
|
+
fs.unlinkSync(lockPath);
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
if (err.code !== 'ENOENT')
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
return tryAcquire();
|
|
150
|
+
}) ?? null);
|
|
151
|
+
}
|
|
152
|
+
function withUpdateStateLock(cachePath, now, update) {
|
|
153
|
+
const release = acquireUpdateStateLock(cachePath, now);
|
|
154
|
+
if (!release)
|
|
155
|
+
return undefined;
|
|
156
|
+
try {
|
|
157
|
+
return update();
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
release();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
export function spawnUpdateCheckWorker(cachePath, spawnImpl = spawn) {
|
|
164
|
+
const workerPath = fileURLToPath(new URL('../update-check-worker.js', import.meta.url));
|
|
165
|
+
const child = spawnImpl(process.execPath, [workerPath, cachePath], {
|
|
166
|
+
detached: true,
|
|
167
|
+
stdio: 'ignore',
|
|
168
|
+
windowsHide: true,
|
|
169
|
+
});
|
|
170
|
+
child.on('error', () => {
|
|
171
|
+
// A missing/broken Node binary must not turn a completed CLI command into a failure.
|
|
172
|
+
});
|
|
173
|
+
child.unref();
|
|
174
|
+
}
|
|
175
|
+
function updateChecksDisabled() {
|
|
176
|
+
const ci = process.env.CI?.trim().toLowerCase();
|
|
177
|
+
return (process.env.PRLL_DISABLE_UPDATE_CHECK === '1' ||
|
|
178
|
+
process.env.NO_UPDATE_NOTIFIER === '1' ||
|
|
179
|
+
Boolean(ci && ci !== '0' && ci !== 'false'));
|
|
180
|
+
}
|
|
181
|
+
export function createUpdateNotifier(currentVersion, options = {}) {
|
|
182
|
+
const now = options.now ?? Date.now;
|
|
183
|
+
const cachePath = options.cachePath ?? defaultCachePath();
|
|
184
|
+
const spawnWorker = options.spawnWorker ?? spawnUpdateCheckWorker;
|
|
185
|
+
const writeHint = options.writeHint ?? ((hint) => process.stderr.write(hint));
|
|
186
|
+
const disabled = options.disabled ?? updateChecksDisabled();
|
|
187
|
+
return {
|
|
188
|
+
afterSuccessfulCommand: () => {
|
|
189
|
+
if (disabled || !releaseParts(currentVersion))
|
|
190
|
+
return;
|
|
191
|
+
const timestamp = now();
|
|
192
|
+
const decision = withUpdateStateLock(cachePath, now, () => {
|
|
193
|
+
const state = readState(cachePath);
|
|
194
|
+
let hint;
|
|
195
|
+
let changed = false;
|
|
196
|
+
if (state.latest_version &&
|
|
197
|
+
isNewerRelease(currentVersion, state.latest_version) &&
|
|
198
|
+
timestamp - (state.last_notified_at ?? 0) >= CHECK_INTERVAL_MS) {
|
|
199
|
+
hint =
|
|
200
|
+
`Update available: parall ${currentVersion} → ${state.latest_version}. ` +
|
|
201
|
+
'Upgrade @parall/cli@latest with your package manager.\n';
|
|
202
|
+
state.last_notified_at = timestamp;
|
|
203
|
+
changed = true;
|
|
204
|
+
}
|
|
205
|
+
const successfulCheckIsFresh = timestamp - (state.last_success_at ?? 0) < CHECK_INTERVAL_MS;
|
|
206
|
+
const recentAttempt = timestamp - (state.last_attempt_at ?? 0) < RETRY_INTERVAL_MS;
|
|
207
|
+
const shouldSpawnWorker = !successfulCheckIsFresh && !recentAttempt;
|
|
208
|
+
if (shouldSpawnWorker) {
|
|
209
|
+
state.last_attempt_at = timestamp;
|
|
210
|
+
changed = true;
|
|
211
|
+
}
|
|
212
|
+
if (changed && !writeState(cachePath, state))
|
|
213
|
+
return undefined;
|
|
214
|
+
return { hint, shouldSpawnWorker };
|
|
215
|
+
});
|
|
216
|
+
if (!decision)
|
|
217
|
+
return;
|
|
218
|
+
if (decision.hint) {
|
|
219
|
+
try {
|
|
220
|
+
writeHint(decision.hint);
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
// A closed output stream must not change the command's exit status.
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (!decision.shouldSpawnWorker)
|
|
227
|
+
return;
|
|
228
|
+
try {
|
|
229
|
+
spawnWorker(cachePath);
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
// Update checks never affect the command that triggered them.
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
export async function refreshUpdateCache(cachePath, options = {}) {
|
|
238
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
239
|
+
const timeoutMs = options.timeoutMs ?? UPDATE_CHECK_TIMEOUT_MS;
|
|
240
|
+
const controller = new AbortController();
|
|
241
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
242
|
+
try {
|
|
243
|
+
const response = await fetchImpl(options.registryUrl ?? REGISTRY_URL, {
|
|
244
|
+
headers: { accept: 'application/json' },
|
|
245
|
+
signal: controller.signal,
|
|
246
|
+
});
|
|
247
|
+
if (!response.ok)
|
|
248
|
+
return;
|
|
249
|
+
const body = (await response.json());
|
|
250
|
+
if (typeof body.version !== 'string' || !releaseParts(body.version))
|
|
251
|
+
return;
|
|
252
|
+
const latestVersion = body.version;
|
|
253
|
+
const now = options.now ?? Date.now;
|
|
254
|
+
withUpdateStateLock(cachePath, now, () => {
|
|
255
|
+
const state = readState(cachePath);
|
|
256
|
+
writeState(cachePath, {
|
|
257
|
+
...state,
|
|
258
|
+
latest_version: latestVersion,
|
|
259
|
+
last_success_at: now(),
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
// Network, timeout, registry, and cache failures are intentionally silent.
|
|
265
|
+
}
|
|
266
|
+
finally {
|
|
267
|
+
clearTimeout(timer);
|
|
268
|
+
}
|
|
269
|
+
}
|
package/dist/lib/upload.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../../src/lib/upload.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AA6BhD,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAGtD;AAED,wBAAsB,UAAU,CAC9B,MAAM,EAAE,YAAY,EACpB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../../src/lib/upload.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AA6BhD,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAGtD;AAED,wBAAsB,UAAU,CAC9B,MAAM,EAAE,YAAY,EACpB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAgFzF"}
|
package/dist/lib/upload.js
CHANGED
|
@@ -39,16 +39,71 @@ export async function uploadFile(client, orgId, filePath) {
|
|
|
39
39
|
file_name: fileName,
|
|
40
40
|
file_size: stat.size,
|
|
41
41
|
mime_type: mimeType,
|
|
42
|
+
multipart_supported: true,
|
|
42
43
|
});
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
44
|
+
let completionStarted = false;
|
|
45
|
+
try {
|
|
46
|
+
let completedParts;
|
|
47
|
+
if (presign.upload_type === 'multipart') {
|
|
48
|
+
if (!presign.part_size || !presign.parts?.length) {
|
|
49
|
+
throw new Error('Server returned an invalid multipart upload response');
|
|
50
|
+
}
|
|
51
|
+
const file = await fs.promises.open(resolved, 'r');
|
|
52
|
+
try {
|
|
53
|
+
completedParts = [];
|
|
54
|
+
const orderedParts = [...presign.parts].sort((a, b) => a.part_number - b.part_number);
|
|
55
|
+
for (const part of orderedParts) {
|
|
56
|
+
const position = (part.part_number - 1) * presign.part_size;
|
|
57
|
+
const length = Math.min(presign.part_size, stat.size - position);
|
|
58
|
+
if (length <= 0) {
|
|
59
|
+
throw new Error(`Upload part ${part.part_number} is outside the file bounds`);
|
|
60
|
+
}
|
|
61
|
+
const buffer = Buffer.allocUnsafe(length);
|
|
62
|
+
let bytesRead = 0;
|
|
63
|
+
while (bytesRead < length) {
|
|
64
|
+
const result = await file.read(buffer, bytesRead, length - bytesRead, position + bytesRead);
|
|
65
|
+
if (result.bytesRead === 0)
|
|
66
|
+
throw new Error('Unexpected end of file');
|
|
67
|
+
bytesRead += result.bytesRead;
|
|
68
|
+
}
|
|
69
|
+
const uploadRes = await fetch(part.upload_url, {
|
|
70
|
+
method: 'PUT',
|
|
71
|
+
body: buffer,
|
|
72
|
+
headers: { 'Content-Type': mimeType },
|
|
73
|
+
});
|
|
74
|
+
if (!uploadRes.ok) {
|
|
75
|
+
throw new Error(`Upload part ${part.part_number} failed: ${uploadRes.status} ${uploadRes.statusText}`);
|
|
76
|
+
}
|
|
77
|
+
const etag = uploadRes.headers.get('ETag');
|
|
78
|
+
if (!etag)
|
|
79
|
+
throw new Error(`Upload part ${part.part_number} returned no ETag`);
|
|
80
|
+
completedParts.push({ part_number: part.part_number, etag });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
await file.close();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
if (!presign.upload_url)
|
|
89
|
+
throw new Error('Server did not return an upload URL');
|
|
90
|
+
const fileBuffer = fs.readFileSync(resolved);
|
|
91
|
+
const uploadRes = await fetch(presign.upload_url, {
|
|
92
|
+
method: 'PUT',
|
|
93
|
+
body: fileBuffer,
|
|
94
|
+
headers: { 'Content-Type': mimeType },
|
|
95
|
+
});
|
|
96
|
+
if (!uploadRes.ok) {
|
|
97
|
+
throw new Error(`Upload failed: ${uploadRes.status} ${uploadRes.statusText}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
completionStarted = true;
|
|
101
|
+
await client.completeUpload(orgId, presign.attachment_id, completedParts);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
if (!completionStarted)
|
|
105
|
+
await client.abortUpload(orgId, presign.attachment_id).catch(() => { });
|
|
106
|
+
throw error;
|
|
51
107
|
}
|
|
52
|
-
await client.completeUpload(orgId, presign.attachment_id);
|
|
53
108
|
return { attachmentId: presign.attachment_id, fileName, fileSize: stat.size, mimeType };
|
|
54
109
|
}
|