@parall/cli 1.55.3 → 1.55.4
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/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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profile.d.ts","sourceRoot":"","sources":["../../src/commands/profile.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAWpC,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,QA8GvD"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { resolveCredentials } from '../lib/client.js';
|
|
3
|
+
import { printJson, printError } from '../lib/output.js';
|
|
4
|
+
// Profile commands follow the platform's uniform permission loop: `set` calls
|
|
5
|
+
// the ATOMIC endpoints directly. Callers with edit power (humans on personal
|
|
6
|
+
// keys) mutate immediately; a denied caller (an agent editing itself) gets the
|
|
7
|
+
// structured 403 whose printed `Request approval:` command carries the exact
|
|
8
|
+
// payload that was attempted — running it files the proposal for a human to
|
|
9
|
+
// decide. There is no separate "propose" verb.
|
|
10
|
+
export function registerProfileCommands(program) {
|
|
11
|
+
const profile = program.command('profile').description('Read and edit your profile');
|
|
12
|
+
profile
|
|
13
|
+
.command('show')
|
|
14
|
+
.description('Show your identity, org profile, Instructions and manager')
|
|
15
|
+
.action(async () => {
|
|
16
|
+
try {
|
|
17
|
+
const { client, orgId } = resolveCredentials();
|
|
18
|
+
const me = await client.getMe();
|
|
19
|
+
const result = { user: me };
|
|
20
|
+
try {
|
|
21
|
+
result.org_profile = await client.getMemberProfile(orgId, me.id);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
/* display-only enrichment */
|
|
25
|
+
}
|
|
26
|
+
if (me.type === 'agent') {
|
|
27
|
+
try {
|
|
28
|
+
result.instructions = await client.getAgentInstructions(orgId, me.id);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
/* not readable */
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
result.manager = await client.getAgentManager(orgId, me.id);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
/* not readable */
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
printJson(result);
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
printError(err);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
profile
|
|
47
|
+
.command('set')
|
|
48
|
+
.description('Edit your profile (title/about, Instructions, display name). If you lack permission, the error prints a ready-made approval request carrying exactly what you tried to write.')
|
|
49
|
+
.option('--title <text>', 'Org-profile title')
|
|
50
|
+
.option('--about <text>', 'Org-profile about/description')
|
|
51
|
+
.option('--instructions <text>', 'Full replacement Instructions')
|
|
52
|
+
.option('--instructions-file <path>', 'Read replacement Instructions from a file')
|
|
53
|
+
.option('--display-name <name>', 'Display name')
|
|
54
|
+
.action(async (opts) => {
|
|
55
|
+
if (opts.instructions !== undefined && opts.instructionsFile !== undefined) {
|
|
56
|
+
printError(new Error('--instructions and --instructions-file are mutually exclusive'));
|
|
57
|
+
}
|
|
58
|
+
const wantsProfile = opts.title !== undefined || opts.about !== undefined;
|
|
59
|
+
const instructionsBody = opts.instructions ??
|
|
60
|
+
(opts.instructionsFile ? readFileSync(opts.instructionsFile, 'utf8') : undefined);
|
|
61
|
+
if (!wantsProfile && instructionsBody === undefined && opts.displayName === undefined) {
|
|
62
|
+
printError(new Error('Nothing to change. Provide --title/--about, --instructions(-file), or --display-name.'));
|
|
63
|
+
}
|
|
64
|
+
const { client, orgId } = resolveCredentials();
|
|
65
|
+
const me = await client.getMe().catch((err) => printError(err));
|
|
66
|
+
const applied = {};
|
|
67
|
+
// Field groups apply independently, in order; the first denial exits
|
|
68
|
+
// with the prefilled proposal command for THAT group.
|
|
69
|
+
if (wantsProfile) {
|
|
70
|
+
// Read-modify-write like any client: a missing profile row is a
|
|
71
|
+
// successful empty read at version 0. Unspecified fields keep their
|
|
72
|
+
// current value so the write (and any resulting proposal) is always
|
|
73
|
+
// the full final state.
|
|
74
|
+
const current = await client.getMemberProfile(orgId, me.id).catch((err) => printError(err));
|
|
75
|
+
const body = {
|
|
76
|
+
title: opts.title ?? current.title,
|
|
77
|
+
description: opts.about ?? current.description,
|
|
78
|
+
expected_version: current.profile_version,
|
|
79
|
+
};
|
|
80
|
+
try {
|
|
81
|
+
applied.org_profile = await client.updateMemberProfile(orgId, me.id, body);
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
printError(err, body);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (instructionsBody !== undefined) {
|
|
88
|
+
const current = await client
|
|
89
|
+
.getAgentInstructions(orgId, me.id)
|
|
90
|
+
.catch((err) => printError(err));
|
|
91
|
+
const body = { instructions: instructionsBody, expected_version: current.version };
|
|
92
|
+
try {
|
|
93
|
+
applied.instructions = await client.updateAgentInstructions(orgId, me.id, body);
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
printError(err, body);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (opts.displayName !== undefined) {
|
|
100
|
+
try {
|
|
101
|
+
applied.user = await client.updateMe({ display_name: opts.displayName });
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
// The identity proposal needs the CAS base (name at filing time),
|
|
105
|
+
// which the direct PATCH body does not carry — prefill it here.
|
|
106
|
+
printError(err, {
|
|
107
|
+
display_name: opts.displayName,
|
|
108
|
+
previous_display_name: me.display_name,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
printJson(applied);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"projects.d.ts","sourceRoot":"","sources":["../../src/commands/projects.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"projects.d.ts","sourceRoot":"","sources":["../../src/commands/projects.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,QAyOvD"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolveCredentials } from '../lib/client.js';
|
|
2
2
|
import { printJson, printError, stripPrllScheme } from '../lib/output.js';
|
|
3
|
+
const VISIBILITY_HELP = 'public (any org member self-joins), restricted (join by approved request), or private (invite only); reading requires roster membership, with org owners/admins implicit';
|
|
3
4
|
export function registerProjectCommands(program) {
|
|
4
5
|
const projects = program.command('projects').description('Manage projects');
|
|
5
6
|
projects
|
|
@@ -35,7 +36,7 @@ export function registerProjectCommands(program) {
|
|
|
35
36
|
.requiredOption('--name <name>', 'Project name')
|
|
36
37
|
.requiredOption('--key <key>', 'Project key')
|
|
37
38
|
.option('--description <text>', 'Project description')
|
|
38
|
-
.option('--visibility <visibility>',
|
|
39
|
+
.option('--visibility <visibility>', VISIBILITY_HELP)
|
|
39
40
|
.option('--color <hex>', 'Project color (hex)')
|
|
40
41
|
.action(async (opts) => {
|
|
41
42
|
try {
|
|
@@ -60,7 +61,7 @@ export function registerProjectCommands(program) {
|
|
|
60
61
|
.argument('<projectId>', 'Project ID')
|
|
61
62
|
.option('--name <name>', 'Project name')
|
|
62
63
|
.option('--description <text>', 'Project description')
|
|
63
|
-
.option('--visibility <visibility>',
|
|
64
|
+
.option('--visibility <visibility>', VISIBILITY_HELP)
|
|
64
65
|
.option('--color <hex>', 'Project color (hex)')
|
|
65
66
|
.option('--status <status>', 'Project status')
|
|
66
67
|
.action(async (projectId, opts) => {
|
|
@@ -102,4 +103,133 @@ export function registerProjectCommands(program) {
|
|
|
102
103
|
printError(err);
|
|
103
104
|
}
|
|
104
105
|
});
|
|
106
|
+
projects
|
|
107
|
+
.command('library')
|
|
108
|
+
.description('Browse joinable projects (public + restricted) with your admission state. If task creation fails with no available projects, start here.')
|
|
109
|
+
.action(async () => {
|
|
110
|
+
try {
|
|
111
|
+
const { client, orgId } = resolveCredentials();
|
|
112
|
+
const result = await client.getProjectLibrary(orgId);
|
|
113
|
+
printJson(result);
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
printError(err);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
projects
|
|
120
|
+
.command('join')
|
|
121
|
+
.description('Join a public project immediately (restricted projects need `projects request-join`)')
|
|
122
|
+
.argument('<projectId>', 'Project ID')
|
|
123
|
+
.action(async (projectId) => {
|
|
124
|
+
try {
|
|
125
|
+
const { client, orgId } = resolveCredentials();
|
|
126
|
+
const result = await client.joinProject(orgId, stripPrllScheme(projectId));
|
|
127
|
+
printJson(result);
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
printError(err);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
projects
|
|
134
|
+
.command('request-join')
|
|
135
|
+
.description('File a join request for a restricted project; a project manager approves it')
|
|
136
|
+
.argument('<projectId>', 'Project ID')
|
|
137
|
+
.option('--cancel', 'Withdraw your pending join request instead')
|
|
138
|
+
.action(async (projectId, opts) => {
|
|
139
|
+
try {
|
|
140
|
+
const { client, orgId } = resolveCredentials();
|
|
141
|
+
const id = stripPrllScheme(projectId);
|
|
142
|
+
if (opts.cancel) {
|
|
143
|
+
await client.cancelProjectJoinRequest(orgId, id);
|
|
144
|
+
printJson({ cancelled: id });
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const result = await client.createProjectJoinRequest(orgId, id);
|
|
148
|
+
printJson(result);
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
printError(err);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
projects
|
|
155
|
+
.command('join-requests')
|
|
156
|
+
.description('List pending join requests for a project (manager standing required)')
|
|
157
|
+
.argument('<projectId>', 'Project ID')
|
|
158
|
+
.action(async (projectId) => {
|
|
159
|
+
try {
|
|
160
|
+
const { client, orgId } = resolveCredentials();
|
|
161
|
+
const result = await client.listProjectJoinRequests(orgId, stripPrllScheme(projectId));
|
|
162
|
+
printJson(result);
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
printError(err);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
const members = projects.command('members').description('Manage a project roster');
|
|
169
|
+
members
|
|
170
|
+
.command('list')
|
|
171
|
+
.description('List roster members (readable by anyone who can read the project)')
|
|
172
|
+
.argument('<projectId>', 'Project ID')
|
|
173
|
+
.action(async (projectId) => {
|
|
174
|
+
try {
|
|
175
|
+
const { client, orgId } = resolveCredentials();
|
|
176
|
+
const result = await client.getProjectMembers(orgId, stripPrllScheme(projectId));
|
|
177
|
+
printJson(result);
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
printError(err);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
members
|
|
184
|
+
.command('add')
|
|
185
|
+
.description('Add a user or team to the roster (manager standing required)')
|
|
186
|
+
.argument('<projectId>', 'Project ID')
|
|
187
|
+
.argument('<subject>', 'User ID (usr_...) or team ID (team_...)')
|
|
188
|
+
.option('--role <role>', 'member or manager', 'member')
|
|
189
|
+
.action(async (projectId, subject, opts) => {
|
|
190
|
+
try {
|
|
191
|
+
const { client, orgId } = resolveCredentials();
|
|
192
|
+
const result = await client.addProjectMember(orgId, stripPrllScheme(projectId), {
|
|
193
|
+
subject: stripPrllScheme(subject),
|
|
194
|
+
role: opts.role,
|
|
195
|
+
});
|
|
196
|
+
printJson(result);
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
printError(err);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
members
|
|
203
|
+
.command('remove')
|
|
204
|
+
.description('Remove a roster entry (manager standing required)')
|
|
205
|
+
.argument('<projectId>', 'Project ID')
|
|
206
|
+
.argument('<subject>', 'User ID or team ID')
|
|
207
|
+
.action(async (projectId, subject) => {
|
|
208
|
+
try {
|
|
209
|
+
const { client, orgId } = resolveCredentials();
|
|
210
|
+
const id = stripPrllScheme(projectId);
|
|
211
|
+
const sub = stripPrllScheme(subject);
|
|
212
|
+
await client.removeProjectMember(orgId, id, sub);
|
|
213
|
+
printJson({ removed: sub, project: id });
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
printError(err);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
members
|
|
220
|
+
.command('set-role')
|
|
221
|
+
.description('Change a roster entry role (manager standing required)')
|
|
222
|
+
.argument('<projectId>', 'Project ID')
|
|
223
|
+
.argument('<subject>', 'User ID or team ID')
|
|
224
|
+
.requiredOption('--role <role>', 'member or manager')
|
|
225
|
+
.action(async (projectId, subject, opts) => {
|
|
226
|
+
try {
|
|
227
|
+
const { client, orgId } = resolveCredentials();
|
|
228
|
+
const result = await client.updateProjectMember(orgId, stripPrllScheme(projectId), stripPrllScheme(subject), { role: opts.role });
|
|
229
|
+
printJson(result);
|
|
230
|
+
}
|
|
231
|
+
catch (err) {
|
|
232
|
+
printError(err);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
105
235
|
}
|
package/dist/commands/refs.d.ts
CHANGED
|
@@ -4,8 +4,9 @@ import type { Command } from 'commander';
|
|
|
4
4
|
* none. --strict forces none and cannot be combined with --from; non-message
|
|
5
5
|
* runtime triggers are never accepted as message authority.
|
|
6
6
|
*/
|
|
7
|
-
export declare function buildResolveRefsOptions(explicitFrom: string | undefined, triggerMessageId: string | undefined, strict?: boolean): {
|
|
8
|
-
sourceMessageId
|
|
7
|
+
export declare function buildResolveRefsOptions(explicitFrom: string | undefined, triggerMessageId: string | undefined, strict?: boolean, full?: boolean): {
|
|
8
|
+
sourceMessageId?: string;
|
|
9
|
+
full?: boolean;
|
|
9
10
|
} | undefined;
|
|
10
11
|
/** Build the SDK ref-graph params from the positional URI + CLI flags. */
|
|
11
12
|
export declare function buildRefGraphParams(uri: string, opts: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"refs.d.ts","sourceRoot":"","sources":["../../src/commands/refs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAUzC;;;;GAIG;AACH,wBAAgB,uBAAuB,CACrC,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,gBAAgB,EAAE,MAAM,GAAG,SAAS,EACpC,MAAM,UAAQ,
|
|
1
|
+
{"version":3,"file":"refs.d.ts","sourceRoot":"","sources":["../../src/commands/refs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAUzC;;;;GAIG;AACH,wBAAgB,uBAAuB,CACrC,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,gBAAgB,EAAE,MAAM,GAAG,SAAS,EACpC,MAAM,UAAQ,EACd,IAAI,UAAQ,GACX;IAAE,eAAe,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,SAAS,CAsB1D;AAED,0EAA0E;AAC1E,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GACvB;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAOjC;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,QA0EnD"}
|
package/dist/commands/refs.js
CHANGED
|
@@ -5,28 +5,30 @@ import { ensurePrllScheme, parsePositiveInt, printError, printJson, stripPrllSch
|
|
|
5
5
|
* none. --strict forces none and cannot be combined with --from; non-message
|
|
6
6
|
* runtime triggers are never accepted as message authority.
|
|
7
7
|
*/
|
|
8
|
-
export function buildResolveRefsOptions(explicitFrom, triggerMessageId, strict = false) {
|
|
8
|
+
export function buildResolveRefsOptions(explicitFrom, triggerMessageId, strict = false, full = false) {
|
|
9
9
|
const explicit = explicitFrom?.trim();
|
|
10
10
|
if (strict && explicit) {
|
|
11
11
|
throw new Error('--strict cannot be combined with --from');
|
|
12
12
|
}
|
|
13
13
|
if (strict)
|
|
14
|
-
return undefined;
|
|
14
|
+
return full ? { full: true } : undefined;
|
|
15
15
|
if (explicit) {
|
|
16
16
|
const sourceMessageId = stripPrllScheme(explicit);
|
|
17
17
|
if (!sourceMessageId.startsWith('msg_')) {
|
|
18
18
|
throw new Error('--from must be a message ID (msg_...) or prll://msg_... URI');
|
|
19
19
|
}
|
|
20
|
-
return { sourceMessageId };
|
|
20
|
+
return full ? { sourceMessageId, full: true } : { sourceMessageId };
|
|
21
21
|
}
|
|
22
22
|
// Typed task dispatches can carry a task ID in trigger_message_id. Only a
|
|
23
23
|
// real message can grant the server's cross-chat reference access; omitting
|
|
24
24
|
// non-message triggers preserves the normal membership-filtered resolve.
|
|
25
25
|
const trigger = triggerMessageId?.trim();
|
|
26
26
|
if (!trigger)
|
|
27
|
-
return undefined;
|
|
27
|
+
return full ? { full: true } : undefined;
|
|
28
28
|
const sourceMessageId = stripPrllScheme(trigger);
|
|
29
|
-
|
|
29
|
+
if (!sourceMessageId.startsWith('msg_'))
|
|
30
|
+
return full ? { full: true } : undefined;
|
|
31
|
+
return full ? { sourceMessageId, full: true } : { sourceMessageId };
|
|
30
32
|
}
|
|
31
33
|
/** Build the SDK ref-graph params from the positional URI + CLI flags. */
|
|
32
34
|
export function buildRefGraphParams(uri, opts) {
|
|
@@ -45,11 +47,15 @@ export function registerRefCommands(program) {
|
|
|
45
47
|
.description('Batch resolve prll:// URIs to entity metadata')
|
|
46
48
|
.option('--from <messageId>', 'Forwarding message for cross-chat access (defaults to trigger)')
|
|
47
49
|
.option('--strict', 'Ignore runtime trigger and enforce direct membership checks')
|
|
50
|
+
.option('--full', 'Return complete text for authorized message references')
|
|
48
51
|
.action(async (uris, opts) => {
|
|
49
52
|
try {
|
|
50
53
|
const { client, orgId } = resolveCredentials();
|
|
51
54
|
const runtime = resolveRuntimeContext();
|
|
52
|
-
|
|
55
|
+
// Normalize schemeless args (msg_x, cht_x#range=…) like `refs graph`
|
|
56
|
+
// does: the server drops non-prll URIs without an error, so a raw id
|
|
57
|
+
// would silently resolve to nothing.
|
|
58
|
+
const result = await client.resolveRefs(orgId, uris.map(ensurePrllScheme), buildResolveRefsOptions(opts.from, runtime.triggerMessageId, opts.strict, opts.full));
|
|
53
59
|
printJson(result);
|
|
54
60
|
}
|
|
55
61
|
catch (err) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tasks.d.ts","sourceRoot":"","sources":["../../src/commands/tasks.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAqB,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"tasks.d.ts","sourceRoot":"","sources":["../../src/commands/tasks.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAqB,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAiEpC,wBAAgB,mBAAmB,CAAC,IAAI,EAAE;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;;;;;;;;;;;GAaA;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;;YAS0B,iBAAiB,CAAC,QAAQ,CAAC;cACvB,iBAAiB,CAAC,UAAU,CAAC;;;;;;;;GAS3D;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,QAkXpD"}
|
package/dist/commands/tasks.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ApiError } from '@parall/sdk';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
2
3
|
import { resolveCredentials, resolveTypedDispatchBinding } from '../lib/client.js';
|
|
3
4
|
import { printJson, printError, printRefHint, stripPrllScheme } from '../lib/output.js';
|
|
4
5
|
function stripUndefined(obj) {
|
|
@@ -201,10 +202,18 @@ export function registerTaskCommands(program) {
|
|
|
201
202
|
const binding = resolveTypedDispatchBinding(taskId);
|
|
202
203
|
const req = { ...data };
|
|
203
204
|
if (binding) {
|
|
204
|
-
|
|
205
|
+
if (binding.lane)
|
|
206
|
+
req.dispatch_lane = binding.lane;
|
|
205
207
|
req.dispatch_event_id = binding.dispatchEventId;
|
|
206
|
-
if (binding.
|
|
207
|
-
req.
|
|
208
|
+
if (binding.generation !== undefined)
|
|
209
|
+
req.dispatch_generation = binding.generation;
|
|
210
|
+
if (binding.effectKey) {
|
|
211
|
+
req.dispatch_effect_key = binding.lane
|
|
212
|
+
? binding.effectKey
|
|
213
|
+
: // Converged: content-address the key so a same-payload retry
|
|
214
|
+
// replays idempotently and a different update mints a new key.
|
|
215
|
+
`${binding.effectKey}:${createHash('sha256').update(JSON.stringify(data)).digest('hex').slice(0, 12)}`;
|
|
216
|
+
}
|
|
208
217
|
}
|
|
209
218
|
const result = await client.updateTask(orgId, taskId, req);
|
|
210
219
|
if (binding?.effectKey) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"teams.d.ts","sourceRoot":"","sources":["../../src/commands/teams.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAOpC,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,QAiGpD"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { resolveCredentials } from '../lib/client.js';
|
|
2
|
+
import { printJson, printError, stripPrllScheme } from '../lib/output.js';
|
|
3
|
+
// Teams are pure permission rosters (ACL subjects for wiki/project ACLs);
|
|
4
|
+
// server-side authorization decides who may mutate them — the CLI only
|
|
5
|
+
// exposes the same surface the web Teams settings use.
|
|
6
|
+
export function registerTeamCommands(program) {
|
|
7
|
+
const teams = program.command('teams').description('Manage permission teams');
|
|
8
|
+
teams
|
|
9
|
+
.command('list')
|
|
10
|
+
.description('List teams in the organization')
|
|
11
|
+
.action(async () => {
|
|
12
|
+
try {
|
|
13
|
+
const { client, orgId } = resolveCredentials();
|
|
14
|
+
const result = await client.getTeams(orgId);
|
|
15
|
+
printJson(result);
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
printError(err);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
teams
|
|
22
|
+
.command('get')
|
|
23
|
+
.description('Get a team by ID')
|
|
24
|
+
.argument('<teamId>', 'Team ID')
|
|
25
|
+
.action(async (teamId) => {
|
|
26
|
+
try {
|
|
27
|
+
const { client, orgId } = resolveCredentials();
|
|
28
|
+
const result = await client.getTeam(orgId, stripPrllScheme(teamId));
|
|
29
|
+
printJson(result);
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
printError(err);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
const members = teams.command('members').description('Manage a team roster');
|
|
36
|
+
members
|
|
37
|
+
.command('list')
|
|
38
|
+
.description('List team members')
|
|
39
|
+
.argument('<teamId>', 'Team ID')
|
|
40
|
+
.action(async (teamId) => {
|
|
41
|
+
try {
|
|
42
|
+
const { client, orgId } = resolveCredentials();
|
|
43
|
+
const result = await client.getTeamMembers(orgId, stripPrllScheme(teamId));
|
|
44
|
+
printJson(result);
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
printError(err);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
members
|
|
51
|
+
.command('add')
|
|
52
|
+
.description('Add a member (team manager or org admin)')
|
|
53
|
+
.argument('<teamId>', 'Team ID')
|
|
54
|
+
.argument('<userId>', 'User ID (usr_...)')
|
|
55
|
+
.option('--role <role>', 'member or manager', 'member')
|
|
56
|
+
.action(async (teamId, userId, opts) => {
|
|
57
|
+
try {
|
|
58
|
+
const { client, orgId } = resolveCredentials();
|
|
59
|
+
await client.addTeamMember(orgId, stripPrllScheme(teamId), {
|
|
60
|
+
user_id: stripPrllScheme(userId),
|
|
61
|
+
role: opts.role,
|
|
62
|
+
});
|
|
63
|
+
printJson({ added: stripPrllScheme(userId), team: stripPrllScheme(teamId) });
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
printError(err);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
members
|
|
70
|
+
.command('remove')
|
|
71
|
+
.description('Remove a member (team manager or org admin)')
|
|
72
|
+
.argument('<teamId>', 'Team ID')
|
|
73
|
+
.argument('<userId>', 'User ID')
|
|
74
|
+
.action(async (teamId, userId) => {
|
|
75
|
+
try {
|
|
76
|
+
const { client, orgId } = resolveCredentials();
|
|
77
|
+
await client.removeTeamMember(orgId, stripPrllScheme(teamId), stripPrllScheme(userId));
|
|
78
|
+
printJson({ removed: stripPrllScheme(userId), team: stripPrllScheme(teamId) });
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
printError(err);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
members
|
|
85
|
+
.command('set-role')
|
|
86
|
+
.description('Change a member role (org admin; managers manage plain members only)')
|
|
87
|
+
.argument('<teamId>', 'Team ID')
|
|
88
|
+
.argument('<userId>', 'User ID')
|
|
89
|
+
.requiredOption('--role <role>', 'member or manager')
|
|
90
|
+
.action(async (teamId, userId, opts) => {
|
|
91
|
+
try {
|
|
92
|
+
const { client, orgId } = resolveCredentials();
|
|
93
|
+
await client.updateTeamMember(orgId, stripPrllScheme(teamId), stripPrllScheme(userId), {
|
|
94
|
+
role: opts.role,
|
|
95
|
+
});
|
|
96
|
+
printJson({ updated: stripPrllScheme(userId), role: opts.role });
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
printError(err);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
@@ -7,9 +7,10 @@ import { Command } from 'commander';
|
|
|
7
7
|
* performs the vendor call in-process.
|
|
8
8
|
*
|
|
9
9
|
* Verb surface = the vendor's in-scope interface aggregated: postText
|
|
10
|
-
* (send),
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* (send), platform-retained context (history), fetchContactsList +
|
|
11
|
+
* getBriefInfo + getChatroomInfo (contacts), getProfile (profile),
|
|
12
|
+
* checkOnline (status). History always emits its stable page JSON; the other
|
|
13
|
+
* verbs render agent-readable text by default and accept `--json`.
|
|
13
14
|
*
|
|
14
15
|
* INTERNAL research preview: the capability is flag-gated to the internal
|
|
15
16
|
* org. Design: docs/engineering-design/wechat-channel-design.md §3, §4.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wechat.d.ts","sourceRoot":"","sources":["../../src/commands/wechat.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKpC
|
|
1
|
+
{"version":3,"file":"wechat.d.ts","sourceRoot":"","sources":["../../src/commands/wechat.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKpC;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,QA0KtD"}
|
package/dist/commands/wechat.js
CHANGED
|
@@ -9,9 +9,10 @@ import { resolveMessageText, TEXT_FILE_OPTION_DESC, TEXT_OPTION_DESC } from '../
|
|
|
9
9
|
* performs the vendor call in-process.
|
|
10
10
|
*
|
|
11
11
|
* Verb surface = the vendor's in-scope interface aggregated: postText
|
|
12
|
-
* (send),
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* (send), platform-retained context (history), fetchContactsList +
|
|
13
|
+
* getBriefInfo + getChatroomInfo (contacts), getProfile (profile),
|
|
14
|
+
* checkOnline (status). History always emits its stable page JSON; the other
|
|
15
|
+
* verbs render agent-readable text by default and accept `--json`.
|
|
15
16
|
*
|
|
16
17
|
* INTERNAL research preview: the capability is flag-gated to the internal
|
|
17
18
|
* org. Design: docs/engineering-design/wechat-channel-design.md §3, §4.
|
|
@@ -52,6 +53,33 @@ export function registerWechatCommands(program) {
|
|
|
52
53
|
printError(err);
|
|
53
54
|
}
|
|
54
55
|
});
|
|
56
|
+
wechat
|
|
57
|
+
.command('history')
|
|
58
|
+
.description('Read platform-retained context for a WeChat DM or group')
|
|
59
|
+
.requiredOption('--conversation <id>', 'Vendor-native conversation id (a friend wxid, or a room id ending in @chatroom)')
|
|
60
|
+
.option('--before <messageId>', 'Read messages strictly older than this external message id')
|
|
61
|
+
.option('--since <time>', 'Inclusive lower bound: RFC3339 timestamp or YYYY-MM-DD')
|
|
62
|
+
.option('--limit <n>', 'Page size (default 20, max 100)')
|
|
63
|
+
.action(async (opts) => {
|
|
64
|
+
try {
|
|
65
|
+
let limit;
|
|
66
|
+
if (opts.limit !== undefined) {
|
|
67
|
+
limit = Number(opts.limit);
|
|
68
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
|
|
69
|
+
throw new Error('--limit must be an integer between 1 and 100');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const { client, orgId } = resolveCredentials();
|
|
73
|
+
printJson(await client.wechatHistory(orgId, opts.conversation, {
|
|
74
|
+
...(opts.before ? { before: opts.before } : {}),
|
|
75
|
+
...(opts.since ? { since: opts.since } : {}),
|
|
76
|
+
...(limit !== undefined ? { limit } : {}),
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
printError(err);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
55
83
|
wechat
|
|
56
84
|
.command('contacts')
|
|
57
85
|
.description('List the account address book with display names: friends, saved groups, followed official accounts')
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,8 @@ import { registerMessageCommands } from './commands/messages.js';
|
|
|
9
9
|
import { registerTaskCommands } from './commands/tasks.js';
|
|
10
10
|
import { registerCommentCommands } from './commands/comments.js';
|
|
11
11
|
import { registerProjectCommands } from './commands/projects.js';
|
|
12
|
+
import { registerProfileCommands } from './commands/profile.js';
|
|
13
|
+
import { registerTeamCommands } from './commands/teams.js';
|
|
12
14
|
import { registerScheduleCommands } from './commands/schedules.js';
|
|
13
15
|
import { registerExternalTriggerCommands } from './commands/external-triggers.js';
|
|
14
16
|
import { registerUserCommands } from './commands/users.js';
|
|
@@ -22,6 +24,7 @@ import { registerMachineCommands } from './commands/machines.js';
|
|
|
22
24
|
import { registerClipCommands } from './commands/clip.js';
|
|
23
25
|
import { registerSlackCommands } from './commands/slack.js';
|
|
24
26
|
import { registerWechatCommands } from './commands/wechat.js';
|
|
27
|
+
import { createUpdateNotifier } from './lib/update-check.js';
|
|
25
28
|
const require = createRequire(import.meta.url);
|
|
26
29
|
const pkg = require('../package.json');
|
|
27
30
|
const program = new Command();
|
|
@@ -37,6 +40,8 @@ registerMessageCommands(program);
|
|
|
37
40
|
registerTaskCommands(program);
|
|
38
41
|
registerCommentCommands(program);
|
|
39
42
|
registerProjectCommands(program);
|
|
43
|
+
registerProfileCommands(program);
|
|
44
|
+
registerTeamCommands(program);
|
|
40
45
|
registerScheduleCommands(program);
|
|
41
46
|
registerExternalTriggerCommands(program);
|
|
42
47
|
registerUserCommands(program);
|
|
@@ -50,4 +55,10 @@ registerMachineCommands(program);
|
|
|
50
55
|
registerClipCommands(program);
|
|
51
56
|
registerSlackCommands(program);
|
|
52
57
|
registerWechatCommands(program);
|
|
58
|
+
const updateNotifier = createUpdateNotifier(pkg.version);
|
|
59
|
+
program.hook('postAction', () => {
|
|
60
|
+
if (process.exitCode === undefined || process.exitCode === 0) {
|
|
61
|
+
updateNotifier.afterSuccessfulCommand();
|
|
62
|
+
}
|
|
63
|
+
});
|
|
53
64
|
program.parse();
|
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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"update-check-worker.d.ts","sourceRoot":"","sources":["../src/update-check-worker.ts"],"names":[],"mappings":""}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parall/cli",
|
|
3
|
-
"version": "1.55.
|
|
3
|
+
"version": "1.55.4",
|
|
4
4
|
"description": "CLI client for Parall — universal agent & human access to Parall API",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -36,14 +36,14 @@
|
|
|
36
36
|
"diff": "^8.0.3",
|
|
37
37
|
"js-yaml": "^4.1.0",
|
|
38
38
|
"zod": "^4.3.6",
|
|
39
|
-
"@parall/agent-core": "1.55.
|
|
40
|
-
"@parall/sdk": "1.55.
|
|
39
|
+
"@parall/agent-core": "1.55.4",
|
|
40
|
+
"@parall/sdk": "1.55.4"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/js-yaml": "^4.0.9",
|
|
44
44
|
"@types/node": "^22.0.0",
|
|
45
45
|
"typescript": "^5.7.0",
|
|
46
|
-
"@parall/agent-core": "1.55.
|
|
46
|
+
"@parall/agent-core": "1.55.4"
|
|
47
47
|
},
|
|
48
48
|
"scripts": {
|
|
49
49
|
"build": "tsc",
|