@ivotoby/postgram-cli 1.38.3 → 1.40.0
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/README.md +88 -0
- package/dist/client.js +4 -1
- package/dist/pgm.js +14 -0
- package/dist/tracking/api.js +142 -0
- package/dist/tracking/config.js +181 -0
- package/dist/tracking/feature-commands.js +298 -0
- package/dist/tracking/feature-sync.js +151 -0
- package/dist/tracking/here.js +280 -0
- package/dist/tracking/hooks.js +375 -0
- package/dist/tracking/local.js +175 -0
- package/dist/tracking/resume.js +43 -0
- package/dist/tracking/scan-discovery.js +295 -0
- package/dist/tracking/scan.js +579 -0
- package/dist/tracking/types.js +14 -0
- package/dist/tracking/where.js +306 -0
- package/dist/tracking/workspace-commands.js +94 -0
- package/dist/tracking/worktree.js +273 -0
- package/integrations/pi/postgram.ts +73 -0
- package/package.json +2 -1
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
// cli/src/tracking/feature-commands.ts: pgm feature, pgm machine, pgm repo.
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { AppError, ErrorCode } from '../errors.js';
|
|
5
|
+
import { parseCommaList, shortId } from '../shared.js';
|
|
6
|
+
import { runTrackingCommand } from './api.js';
|
|
7
|
+
import { UsageError, usageExit, writeMachineConfig } from './config.js';
|
|
8
|
+
import { readBootId, removeMarker, resolveLocalWorkspace, writeMarker } from './local.js';
|
|
9
|
+
import { formatSync, runFeatureSync, runGhPrState } from './feature-sync.js';
|
|
10
|
+
import { FEATURE_STAGES } from './types.js';
|
|
11
|
+
export async function runFeatureAdd(title, options, deps) {
|
|
12
|
+
const tags = parseCommaList(options.tags);
|
|
13
|
+
const response = await deps.client.createFeature({
|
|
14
|
+
content: title,
|
|
15
|
+
...(tags !== undefined ? { tags } : {}),
|
|
16
|
+
...(options.visibility !== undefined ? { visibility: options.visibility } : {})
|
|
17
|
+
});
|
|
18
|
+
return deps.json ? response : [response.entity.id];
|
|
19
|
+
}
|
|
20
|
+
export async function runFeatureStage(id, stage, deps) {
|
|
21
|
+
if (!FEATURE_STAGES.includes(stage)) {
|
|
22
|
+
throw new UsageError(`invalid stage '${stage}'. Use one of: ${FEATURE_STAGES.join(', ')}`);
|
|
23
|
+
}
|
|
24
|
+
const response = await deps.client.setStage(id, stage);
|
|
25
|
+
return deps.json
|
|
26
|
+
? response
|
|
27
|
+
: [`${shortId(response.entity.id)} ${response.previous_stage ?? '-'} → ${stage}`];
|
|
28
|
+
}
|
|
29
|
+
const FULL_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
30
|
+
const SHORT_HEX = /^[0-9a-f]{8}$/i;
|
|
31
|
+
async function resolveTaskId(id, deps) {
|
|
32
|
+
if (FULL_UUID.test(id)) {
|
|
33
|
+
return id.toLowerCase();
|
|
34
|
+
}
|
|
35
|
+
if (!SHORT_HEX.test(id)) {
|
|
36
|
+
throw new UsageError('feature id must be a full UUID or an 8-hex short id');
|
|
37
|
+
}
|
|
38
|
+
const prefix = id.toLowerCase();
|
|
39
|
+
const matches = [];
|
|
40
|
+
for (let offset = 0;; offset += 100) {
|
|
41
|
+
const page = await deps.pgm.listTasks({ limit: 100, offset });
|
|
42
|
+
for (const item of page.items) {
|
|
43
|
+
if (item.id.startsWith(prefix)) {
|
|
44
|
+
matches.push(item.id);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (page.items.length < 100) {
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (matches.length > 1) {
|
|
52
|
+
throw new AppError(ErrorCode.CONFLICT, `feature id ${id} is ambiguous`);
|
|
53
|
+
}
|
|
54
|
+
const [match] = matches;
|
|
55
|
+
if (match === undefined) {
|
|
56
|
+
throw new AppError(ErrorCode.NOT_FOUND, `no feature matches ${id}`);
|
|
57
|
+
}
|
|
58
|
+
return match;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Marks the feature's task done. `pgm where` already hides done tasks unless
|
|
62
|
+
* --all is given, so a feature needs no stage of its own for this.
|
|
63
|
+
*/
|
|
64
|
+
export async function runFeatureDone(id, deps) {
|
|
65
|
+
const taskId = await resolveTaskId(id, deps);
|
|
66
|
+
const current = await deps.pgm.recallEntity(taskId);
|
|
67
|
+
if (current.entity.status === 'done') {
|
|
68
|
+
return deps.json
|
|
69
|
+
? { id: taskId, status: 'done', changed: false }
|
|
70
|
+
: [`${shortId(taskId)} already done`];
|
|
71
|
+
}
|
|
72
|
+
const completed = await deps.pgm.completeTask(taskId, current.entity.version);
|
|
73
|
+
return deps.json
|
|
74
|
+
? { id: taskId, status: completed.entity.status, changed: true }
|
|
75
|
+
: [`${shortId(taskId)} done`];
|
|
76
|
+
}
|
|
77
|
+
const PR_STATES = ['open', 'merged', 'closed'];
|
|
78
|
+
export async function runFeaturePr(id, url, options, deps) {
|
|
79
|
+
const state = options.state ?? 'open';
|
|
80
|
+
if (!PR_STATES.includes(state)) {
|
|
81
|
+
throw new UsageError(`invalid --state '${state}'. Use one of: ${PR_STATES.join(', ')}`);
|
|
82
|
+
}
|
|
83
|
+
const response = await deps.client.addPullRequest(id, {
|
|
84
|
+
url,
|
|
85
|
+
state: state
|
|
86
|
+
});
|
|
87
|
+
return deps.json
|
|
88
|
+
? response
|
|
89
|
+
: [`${shortId(response.entity.id)} PR ${state}, stage ${response.to}`];
|
|
90
|
+
}
|
|
91
|
+
export async function runFeatureLink(id, options, deps) {
|
|
92
|
+
if (options.workspace !== undefined) {
|
|
93
|
+
const patched = await deps.client.patchWorkspace(options.workspace, {
|
|
94
|
+
feature_id: id
|
|
95
|
+
});
|
|
96
|
+
const workspace = patched.workspace;
|
|
97
|
+
if (workspace.machine === deps.config.alias && deps.exists(workspace.path)) {
|
|
98
|
+
if (id !== null) {
|
|
99
|
+
await deps.writeMarker(workspace.path, workspace.feature_id ?? id);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
await deps.removeMarker(workspace.path);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
deps.warn(`warning: ${workspace.machine} may have a feature marker; its next registration will restore the marker's link`);
|
|
107
|
+
}
|
|
108
|
+
return deps.json
|
|
109
|
+
? patched
|
|
110
|
+
: [
|
|
111
|
+
id !== null
|
|
112
|
+
? `linked ${workspace.path} → ${shortId(id)}`
|
|
113
|
+
: `unlinked ${workspace.path}`
|
|
114
|
+
];
|
|
115
|
+
}
|
|
116
|
+
const target = options.path ?? deps.cwd;
|
|
117
|
+
const local = await deps.resolveWorkspace(target);
|
|
118
|
+
if (local.kind === 'timeout') {
|
|
119
|
+
throw new AppError(ErrorCode.INTERNAL, 'git timeout');
|
|
120
|
+
}
|
|
121
|
+
const looked = await deps.client.lookup(deps.config.alias, local.path);
|
|
122
|
+
let workspaceId;
|
|
123
|
+
if (looked.workspace !== null && looked.workspace.path === local.path) {
|
|
124
|
+
workspaceId = looked.workspace.id;
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
const report = await deps.client.report({
|
|
128
|
+
source: 'hook',
|
|
129
|
+
machine: {
|
|
130
|
+
alias: deps.config.alias,
|
|
131
|
+
hostname: deps.hostname,
|
|
132
|
+
boot_id: await deps.bootId()
|
|
133
|
+
},
|
|
134
|
+
workspace: local.workspace
|
|
135
|
+
});
|
|
136
|
+
if (report.workspace === null) {
|
|
137
|
+
throw new AppError(ErrorCode.INTERNAL, 'workspace registration returned no workspace');
|
|
138
|
+
}
|
|
139
|
+
workspaceId = report.workspace.id;
|
|
140
|
+
}
|
|
141
|
+
const patched = await deps.client.patchWorkspace(workspaceId, {
|
|
142
|
+
feature_id: id
|
|
143
|
+
});
|
|
144
|
+
if (id !== null) {
|
|
145
|
+
await deps.writeMarker(local.path, patched.workspace.feature_id ?? id);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
await deps.removeMarker(local.path);
|
|
149
|
+
}
|
|
150
|
+
return deps.json
|
|
151
|
+
? patched
|
|
152
|
+
: [
|
|
153
|
+
id !== null
|
|
154
|
+
? `linked ${local.path} → ${shortId(id)}`
|
|
155
|
+
: `unlinked ${local.path}`
|
|
156
|
+
];
|
|
157
|
+
}
|
|
158
|
+
export async function runMachineSet(options, deps) {
|
|
159
|
+
const written = await deps.writeConfig({
|
|
160
|
+
...(options.alias !== undefined ? { alias: options.alias } : {}),
|
|
161
|
+
...(options.connect !== undefined ? { connectTarget: options.connect } : {}),
|
|
162
|
+
homeDir: deps.homeDir
|
|
163
|
+
});
|
|
164
|
+
if (written.previousAlias !== null && written.previousAlias !== written.alias) {
|
|
165
|
+
deps.warn(`warning: history stays under the old alias '${written.previousAlias}'`);
|
|
166
|
+
}
|
|
167
|
+
await deps.client.report({
|
|
168
|
+
source: 'hook',
|
|
169
|
+
machine: {
|
|
170
|
+
alias: written.alias,
|
|
171
|
+
hostname: deps.hostname,
|
|
172
|
+
boot_id: await deps.bootId()
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
const result = await deps.client.putMachine(written.alias, written.connectTarget);
|
|
176
|
+
return deps.json
|
|
177
|
+
? result
|
|
178
|
+
: [`machine ${written.alias} → ${written.connectTarget}`];
|
|
179
|
+
}
|
|
180
|
+
export async function runMachineList(deps) {
|
|
181
|
+
const result = await deps.client.listMachines();
|
|
182
|
+
return deps.json
|
|
183
|
+
? result
|
|
184
|
+
: result.machines.map((machine) => `${machine.alias} ${machine.connect_target ?? machine.alias} ${machine.last_seen_at}`);
|
|
185
|
+
}
|
|
186
|
+
export async function runRepoSet(remote, options, deps) {
|
|
187
|
+
if (options.releaseOnMerge !== 'true' && options.releaseOnMerge !== 'false') {
|
|
188
|
+
throw new UsageError(`--release-on-merge must be 'true' or 'false'`);
|
|
189
|
+
}
|
|
190
|
+
const releaseOnMerge = options.releaseOnMerge === 'true';
|
|
191
|
+
const result = await deps.client.putRepository(remote, releaseOnMerge);
|
|
192
|
+
return deps.json ? result : [`${remote} release_on_merge=${releaseOnMerge}`];
|
|
193
|
+
}
|
|
194
|
+
function featureDeps(context) {
|
|
195
|
+
return {
|
|
196
|
+
client: context.client,
|
|
197
|
+
config: context.config,
|
|
198
|
+
json: context.json,
|
|
199
|
+
cwd: process.cwd(),
|
|
200
|
+
hostname: os.hostname(),
|
|
201
|
+
homeDir: context.homeDir,
|
|
202
|
+
bootId: readBootId,
|
|
203
|
+
resolveWorkspace: (cwd) => resolveLocalWorkspace(cwd),
|
|
204
|
+
writeMarker: (worktree, featureId) => writeMarker(worktree, featureId, new Date()),
|
|
205
|
+
removeMarker,
|
|
206
|
+
exists: existsSync,
|
|
207
|
+
warn: (line) => process.stderr.write(`${line}\n`)
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
export function registerFeatureCommands(program) {
|
|
211
|
+
const feature = usageExit(program
|
|
212
|
+
.command('feature')
|
|
213
|
+
.description('Manage features'));
|
|
214
|
+
usageExit(feature
|
|
215
|
+
.command('add')
|
|
216
|
+
.description('Create a feature')
|
|
217
|
+
.argument('<title>', 'feature title')
|
|
218
|
+
.option('--tags <tags>', 'comma-separated tags')
|
|
219
|
+
.option('--visibility <visibility>', 'personal, work or shared')).action(async (title, options, command) => {
|
|
220
|
+
await runTrackingCommand(command, (context) => runFeatureAdd(title, options, featureDeps(context)));
|
|
221
|
+
});
|
|
222
|
+
usageExit(feature
|
|
223
|
+
.command('stage')
|
|
224
|
+
.description('Set a feature stage')
|
|
225
|
+
.argument('<id>', 'feature id (UUID or 8-hex)')
|
|
226
|
+
.argument('<stage>', `one of: ${FEATURE_STAGES.join(', ')}`)).action(async (id, stage, _options, command) => {
|
|
227
|
+
await runTrackingCommand(command, (context) => runFeatureStage(id, stage, featureDeps(context)));
|
|
228
|
+
});
|
|
229
|
+
usageExit(feature
|
|
230
|
+
.command('pr')
|
|
231
|
+
.description('Attach a pull request to a feature')
|
|
232
|
+
.argument('<id>', 'feature id (UUID or 8-hex)')
|
|
233
|
+
.argument('<url>', 'pull request URL')
|
|
234
|
+
.option('--state <state>', 'open, merged or closed', 'open')).action(async (id, url, options, command) => {
|
|
235
|
+
await runTrackingCommand(command, (context) => runFeaturePr(id, url, options, featureDeps(context)));
|
|
236
|
+
});
|
|
237
|
+
usageExit(feature
|
|
238
|
+
.command('done')
|
|
239
|
+
.description('Mark a feature done so pgm where hides it (pgm where --all shows it)')
|
|
240
|
+
.argument('<id>', 'feature id (UUID or 8-hex)')).action(async (id, _options, command) => {
|
|
241
|
+
await runTrackingCommand(command, (context) => runFeatureDone(id, { pgm: context.pgm, json: context.json }));
|
|
242
|
+
});
|
|
243
|
+
usageExit(feature
|
|
244
|
+
.command('sync')
|
|
245
|
+
.description('Update PR states from GitHub (needs gh) and apply the stage rules')
|
|
246
|
+
.argument('[id]', 'only this feature (UUID or 8-hex)')).action(async (id, _options, command) => {
|
|
247
|
+
await runTrackingCommand(command, async (context) => {
|
|
248
|
+
const result = await runFeatureSync(id, {
|
|
249
|
+
client: context.client,
|
|
250
|
+
gh: (url) => runGhPrState(url)
|
|
251
|
+
});
|
|
252
|
+
if (result.errors.length > 0) {
|
|
253
|
+
process.exitCode = 1;
|
|
254
|
+
}
|
|
255
|
+
return context.json ? result : formatSync(result);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
usageExit(feature
|
|
259
|
+
.command('link')
|
|
260
|
+
.description('Link a workspace to a feature')
|
|
261
|
+
.argument('<id>', 'feature id (UUID or 8-hex)')
|
|
262
|
+
.option('--path <dir>', 'workspace directory (default: cwd)')
|
|
263
|
+
.option('--workspace <workspace-uuid>', 'link by workspace id instead')).action(async (id, options, command) => {
|
|
264
|
+
await runTrackingCommand(command, (context) => runFeatureLink(id, options, featureDeps(context)));
|
|
265
|
+
});
|
|
266
|
+
usageExit(feature
|
|
267
|
+
.command('unlink')
|
|
268
|
+
.description('Unlink a workspace from its feature')
|
|
269
|
+
.option('--path <dir>', 'workspace directory (default: cwd)')
|
|
270
|
+
.option('--workspace <workspace-uuid>', 'unlink by workspace id instead')).action(async (options, command) => {
|
|
271
|
+
await runTrackingCommand(command, (context) => runFeatureLink(null, options, featureDeps(context)));
|
|
272
|
+
});
|
|
273
|
+
const machine = usageExit(program
|
|
274
|
+
.command('machine')
|
|
275
|
+
.description('Machine settings'));
|
|
276
|
+
usageExit(machine
|
|
277
|
+
.command('set')
|
|
278
|
+
.description('Write ~/.pgmrc and register the machine')
|
|
279
|
+
.option('--alias <alias>', 'machine alias')
|
|
280
|
+
.option('--connect <target>', 'connect target')).action(async (options, command) => {
|
|
281
|
+
await runTrackingCommand(command, (context) => runMachineSet(options, { ...featureDeps(context), writeConfig: writeMachineConfig }));
|
|
282
|
+
});
|
|
283
|
+
usageExit(machine
|
|
284
|
+
.command('list')
|
|
285
|
+
.description('List machines')).action(async (_options, command) => {
|
|
286
|
+
await runTrackingCommand(command, (context) => runMachineList(featureDeps(context)));
|
|
287
|
+
});
|
|
288
|
+
const repo = usageExit(program
|
|
289
|
+
.command('repo')
|
|
290
|
+
.description('Repository settings'));
|
|
291
|
+
usageExit(repo
|
|
292
|
+
.command('set')
|
|
293
|
+
.description('Set repository settings')
|
|
294
|
+
.argument('<remote>', 'repository remote')
|
|
295
|
+
.option('--release-on-merge <bool>', 'true or false')).action(async (remote, options, command) => {
|
|
296
|
+
await runTrackingCommand(command, (context) => runRepoSet(remote, options, featureDeps(context)));
|
|
297
|
+
});
|
|
298
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// cli/src/tracking/feature-sync.ts: pgm feature sync — follow GitHub PR state.
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import { AppError, ErrorCode } from '../errors.js';
|
|
4
|
+
import { shortId } from '../shared.js';
|
|
5
|
+
import { UsageError } from './config.js';
|
|
6
|
+
const GH_STATES = {
|
|
7
|
+
OPEN: 'open',
|
|
8
|
+
MERGED: 'merged',
|
|
9
|
+
CLOSED: 'closed'
|
|
10
|
+
};
|
|
11
|
+
export function parseGhState(stdout) {
|
|
12
|
+
let parsed;
|
|
13
|
+
try {
|
|
14
|
+
parsed = JSON.parse(stdout);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const state = parsed.state;
|
|
23
|
+
return typeof state === 'string' ? (GH_STATES[state] ?? null) : null;
|
|
24
|
+
}
|
|
25
|
+
export function classifyGhFailure(error) {
|
|
26
|
+
const stderr = error.stderr ?? '';
|
|
27
|
+
if (error.code === 'ENOENT') {
|
|
28
|
+
return { ok: false, kind: 'missing', message: 'gh is not installed' };
|
|
29
|
+
}
|
|
30
|
+
if (/gh auth login|not logged in|not logged into|authentication/i.test(stderr)) {
|
|
31
|
+
return { ok: false, kind: 'unauthenticated', message: 'gh is not logged in' };
|
|
32
|
+
}
|
|
33
|
+
const first = stderr.split('\n').find((line) => line.trim() !== '');
|
|
34
|
+
return { ok: false, kind: 'error', message: first?.trim() ?? 'gh failed' };
|
|
35
|
+
}
|
|
36
|
+
const defaultExec = (cmd, args) => new Promise((resolve, reject) => {
|
|
37
|
+
execFile(cmd, args, { timeout: 20_000 }, (error, stdout, stderr) => {
|
|
38
|
+
if (error !== null) {
|
|
39
|
+
reject(Object.assign(error, { stderr }));
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
resolve({ stdout });
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
export async function runGhPrState(url, exec = defaultExec) {
|
|
47
|
+
let stdout;
|
|
48
|
+
try {
|
|
49
|
+
({ stdout } = await exec('gh', ['pr', 'view', url, '--json', 'state']));
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
const failure = error;
|
|
53
|
+
return classifyGhFailure({ code: failure.code, stderr: failure.stderr });
|
|
54
|
+
}
|
|
55
|
+
const state = parseGhState(stdout);
|
|
56
|
+
return state === null
|
|
57
|
+
? { ok: false, kind: 'error', message: 'unexpected output from gh pr view' }
|
|
58
|
+
: { ok: true, state };
|
|
59
|
+
}
|
|
60
|
+
function collectFeatures(response) {
|
|
61
|
+
const features = [];
|
|
62
|
+
for (const group of response.groups) {
|
|
63
|
+
if ('features' in group) {
|
|
64
|
+
features.push(...group.features);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return features;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Asks gh for the state of every PR that is not merged yet and sends changes
|
|
71
|
+
* through the feature PR endpoint, so the server's derived-stage rules apply.
|
|
72
|
+
* A merged PR is final and is not asked again.
|
|
73
|
+
*/
|
|
74
|
+
export async function runFeatureSync(id, deps) {
|
|
75
|
+
const response = await deps.client.where(id === undefined
|
|
76
|
+
? { view: 'stage' }
|
|
77
|
+
: { view: 'stage', includeReleased: true, includeDone: true });
|
|
78
|
+
let features = collectFeatures(response);
|
|
79
|
+
if (id !== undefined) {
|
|
80
|
+
const wanted = id.toLowerCase();
|
|
81
|
+
features = features.filter((feature) => feature.id === wanted || feature.id.startsWith(wanted));
|
|
82
|
+
if (features.length === 0) {
|
|
83
|
+
throw new AppError(ErrorCode.NOT_FOUND, `no feature matches ${id}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const result = { checked: 0, unchanged: 0, changed: [], errors: [] };
|
|
87
|
+
const answers = new Map();
|
|
88
|
+
for (const feature of features) {
|
|
89
|
+
for (const pull of feature.pull_requests) {
|
|
90
|
+
if (pull.state === 'merged') {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
let answer = answers.get(pull.url);
|
|
94
|
+
if (answer === undefined) {
|
|
95
|
+
answer = await deps.gh(pull.url);
|
|
96
|
+
answers.set(pull.url, answer);
|
|
97
|
+
}
|
|
98
|
+
if (!answer.ok && answer.kind === 'missing') {
|
|
99
|
+
throw new UsageError('gh (GitHub CLI) was not found on PATH. Install it from https://cli.github.com to use `pgm feature sync`.');
|
|
100
|
+
}
|
|
101
|
+
if (!answer.ok && answer.kind === 'unauthenticated') {
|
|
102
|
+
throw new UsageError('gh is not logged in. Run `gh auth login`, then retry `pgm feature sync`.');
|
|
103
|
+
}
|
|
104
|
+
result.checked += 1;
|
|
105
|
+
if (!answer.ok) {
|
|
106
|
+
result.errors.push({
|
|
107
|
+
feature_id: feature.id,
|
|
108
|
+
url: pull.url,
|
|
109
|
+
error: answer.message
|
|
110
|
+
});
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (answer.state === pull.state) {
|
|
114
|
+
result.unchanged += 1;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
const updated = await deps.client.addPullRequest(feature.id, {
|
|
119
|
+
url: pull.url,
|
|
120
|
+
state: answer.state
|
|
121
|
+
});
|
|
122
|
+
result.changed.push({
|
|
123
|
+
feature_id: feature.id,
|
|
124
|
+
url: pull.url,
|
|
125
|
+
from: pull.state,
|
|
126
|
+
to: answer.state,
|
|
127
|
+
stage_from: updated.from,
|
|
128
|
+
stage_to: updated.to
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
result.errors.push({
|
|
133
|
+
feature_id: feature.id,
|
|
134
|
+
url: pull.url,
|
|
135
|
+
error: error instanceof Error ? error.message : 'update failed'
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
export function formatSync(result) {
|
|
143
|
+
const lines = result.changed.map((change) => `${shortId(change.feature_id)} ${change.url} ${change.from} → ${change.to}${change.stage_from !== change.stage_to
|
|
144
|
+
? `, stage ${change.stage_from} → ${change.stage_to}`
|
|
145
|
+
: ''}`);
|
|
146
|
+
for (const error of result.errors) {
|
|
147
|
+
lines.push(`error ${shortId(error.feature_id)} ${error.url}: ${error.error}`);
|
|
148
|
+
}
|
|
149
|
+
lines.push(`checked ${result.checked} pull requests: ${result.changed.length} changed, ${result.unchanged} unchanged, ${result.errors.length} failed`);
|
|
150
|
+
return lines;
|
|
151
|
+
}
|