@magnusekdahl/parallix 1.1.0 → 1.2.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/config/integration-pipelines.json +9 -0
- package/config/workflow.config.schema.json +12 -0
- package/docs/adr/0032-mission-refinement-state-and-usage-budget-signals.md +10 -8
- package/docs/adr/0036-mission-sizing-and-dependency-wave-heuristics.md +14 -12
- package/docs/adr/0041-integration-pipeline-gates.md +1 -1
- package/docs/adr/0047-per-mission-change-size-budget.md +161 -0
- package/docs/adr/index.md +1 -0
- package/docs/use-cases.md +44 -0
- package/lib/agents/agents.js +149 -100
- package/lib/agents/claude-telemetry.js +14 -10
- package/lib/agents/claude.js +32 -11
- package/lib/agents/codex-telemetry.js +32 -19
- package/lib/agents/codex.js +36 -11
- package/lib/agents/limit-hit.js +42 -25
- package/lib/agents/mistral-telemetry.js +1 -1
- package/lib/agents/mistral.js +13 -3
- package/lib/agents/opencode-export.js +11 -5
- package/lib/agents/opencode-telemetry.js +75 -90
- package/lib/agents/opencode.js +86 -29
- package/lib/agents/stage-telemetry.js +3 -7
- package/lib/commands/active.js +139 -94
- package/lib/commands/checkpoint.js +3 -1
- package/lib/commands/config.js +5 -3
- package/lib/commands/coverage-gate.js +17 -10
- package/lib/commands/diff.js +11 -5
- package/lib/commands/draft.js +95 -55
- package/lib/commands/handoff.js +253 -65
- package/lib/commands/integrate.js +288 -188
- package/lib/commands/mission-start.js +39 -34
- package/lib/commands/rebase.js +32 -21
- package/lib/commands/repair-handoff.js +21 -18
- package/lib/commands/resolve-conflict.js +5 -2
- package/lib/commands/review.js +1 -0
- package/lib/commands/setup-review.js +1 -0
- package/lib/commands/stats-backfill.js +50 -41
- package/lib/commands/stats.js +653 -204
- package/lib/commands/status.js +33 -28
- package/lib/core/fmt.js +58 -15
- package/lib/core/git.js +7 -5
- package/lib/core/gitignore.js +6 -4
- package/lib/core/mission-utils.js +161 -88
- package/lib/core/nels.js +199 -0
- package/lib/core/persistent-data-migration.js +55 -23
- package/lib/core/product-config.js +44 -23
- package/lib/core/runtime-matrix.js +15 -2
- package/lib/core/spawn-tee.js +40 -13
- package/lib/core/state-map.js +32 -13
- package/lib/core/storage.js +19 -5
- package/lib/core/subagent-limit.js +28 -0
- package/lib/core/verification.js +29 -12
- package/lib/review/rebase.js +11 -3
- package/lib/review/review-adapter.js +93 -6
- package/lib/review/review-artifacts.js +89 -23
- package/lib/review/review-commands.js +139 -45
- package/lib/review/review-events.js +103 -52
- package/lib/review/review-loop.js +117 -87
- package/lib/review/review-polling.js +25 -5
- package/lib/review/review-prompts.js +36 -5
- package/lib/review/review-state.js +39 -15
- package/lib/tools/backlog.js +162 -71
- package/lib/tools/forgejo.js +290 -129
- package/lib/tools/gatekeeper.js +15 -0
- package/lib/tools/redgreen.js +220 -0
- package/lib/tools/sessions.js +12 -5
- package/lib/tools/setup-review.js +170 -70
- package/package.json +11 -3
- package/prompts/draft.md +6 -0
- package/templates/mission-scaffold.md +1 -1
package/lib/commands/status.js
CHANGED
|
@@ -3,12 +3,13 @@ const { findTaskFile, getTaskStatus } = require('../tools/backlog');
|
|
|
3
3
|
const { findMissionDir, findCheckpoints, getFirstLine, inferSlug, missionBranchPrefix, missionBranchName } = require('../core/mission-utils');
|
|
4
4
|
const { WORKFLOW_AGENT_NAMES, eligibleAgentsForStep, readAgentConfigOrExit, workflowLauncherStatus } = require('../agents/agents');
|
|
5
5
|
const { getPrStatus } = require('../tools/forgejo');
|
|
6
|
-
const fs = require('fs');
|
|
7
6
|
const path = require('path');
|
|
8
7
|
const fmt = require('../core/fmt');
|
|
9
8
|
|
|
9
|
+
/** @param {string} porcelain */
|
|
10
10
|
function parseWorktreeList(porcelain) {
|
|
11
11
|
const entries = [];
|
|
12
|
+
/** @type{{path: string, branch: string | null} | null} */
|
|
12
13
|
let current = null;
|
|
13
14
|
|
|
14
15
|
for (const line of porcelain.split('\n')) {
|
|
@@ -21,7 +22,7 @@ function parseWorktreeList(porcelain) {
|
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
if (line.startsWith('worktree ')) {
|
|
24
|
-
if (current) entries.push(current);
|
|
25
|
+
if (current) {entries.push(current);}
|
|
25
26
|
current = { path: line.slice('worktree '.length).trim(), branch: null };
|
|
26
27
|
continue;
|
|
27
28
|
}
|
|
@@ -31,7 +32,7 @@ function parseWorktreeList(porcelain) {
|
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
if (current) entries.push(current);
|
|
35
|
+
if (current) {entries.push(current);}
|
|
35
36
|
return entries;
|
|
36
37
|
}
|
|
37
38
|
|
|
@@ -46,6 +47,7 @@ function findStaleMissionWorktrees({
|
|
|
46
47
|
return [];
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
/** @type {string | null} */
|
|
49
51
|
let resolvedPrimary = primaryWorktree;
|
|
50
52
|
if (!resolvedPrimary) {
|
|
51
53
|
try {
|
|
@@ -65,7 +67,7 @@ function findStaleMissionWorktrees({
|
|
|
65
67
|
const match = branchRef.startsWith(normalizedRefPrefix)
|
|
66
68
|
? [branchRef, branchRef.slice(normalizedRefPrefix.length)]
|
|
67
69
|
: null;
|
|
68
|
-
if (!match) return null;
|
|
70
|
+
if (!match) {return null;}
|
|
69
71
|
|
|
70
72
|
const slug = match[1];
|
|
71
73
|
const taskFile = findTaskFileFn(slug);
|
|
@@ -87,11 +89,13 @@ function findStaleMissionWorktrees({
|
|
|
87
89
|
.filter(Boolean);
|
|
88
90
|
}
|
|
89
91
|
|
|
92
|
+
/** @param {string} ref */
|
|
90
93
|
function formatWorktreeBranch(ref) {
|
|
91
|
-
if (!ref) return '(detached HEAD)';
|
|
94
|
+
if (!ref) {return '(detached HEAD)';}
|
|
92
95
|
return ref.replace(/^refs\/heads\//, '');
|
|
93
96
|
}
|
|
94
97
|
|
|
98
|
+
/** @param {Function} log @param {string} label @param {{detached?: boolean, unmergedFiles: string[]}} rebaseState */
|
|
95
99
|
function logRebaseDiagnostics(log, label, rebaseState) {
|
|
96
100
|
const detachedText = rebaseState.detached ? 'detached HEAD, ' : '';
|
|
97
101
|
log(`${label}: ${detachedText}${rebaseState.unmergedFiles.length} unmerged file(s)`);
|
|
@@ -100,25 +104,26 @@ function logRebaseDiagnostics(log, label, rebaseState) {
|
|
|
100
104
|
});
|
|
101
105
|
}
|
|
102
106
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
-
const
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
const
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
const
|
|
116
|
-
const
|
|
117
|
-
const
|
|
118
|
-
const
|
|
119
|
-
const
|
|
120
|
-
const
|
|
121
|
-
const
|
|
107
|
+
/** @param {string[]} args @param {{exit?: Function, log?: Function, inferSlugFn?: Function, getCurrentBranchFn?: Function, findTaskFileFn?: Function, getTaskStatusFn?: Function, findMissionDirFn?: Function, findCheckpointsFn?: Function, getFirstLineFn?: Function, getPrStatusFn?: Function, findStaleMissionWorktreesFn?: Function, readAgentConfigOrExitFn?: Function, eligibleAgentsForStepFn?: Function, allWorkflowAgentNamesFn?: Function, workflowLauncherStatusFn?: Function, getLastThreeCommitsFn?: Function, getUncommittedCountFn?: Function, detectRebaseStateFn?: Function}} opts */
|
|
108
|
+
function status(args, opts = {}) {
|
|
109
|
+
const exit = opts.exit || process.exit;
|
|
110
|
+
const log = opts.log || fmt.log.plain;
|
|
111
|
+
const inferSlugFn = opts.inferSlugFn || inferSlug;
|
|
112
|
+
const getCurrentBranchFn = opts.getCurrentBranchFn || getCurrentBranch;
|
|
113
|
+
const findTaskFileFn = opts.findTaskFileFn || findTaskFile;
|
|
114
|
+
const getTaskStatusFn = opts.getTaskStatusFn || getTaskStatus;
|
|
115
|
+
const findMissionDirFn = opts.findMissionDirFn || findMissionDir;
|
|
116
|
+
const findCheckpointsFn = opts.findCheckpointsFn || findCheckpoints;
|
|
117
|
+
const getFirstLineFn = opts.getFirstLineFn || getFirstLine;
|
|
118
|
+
const getPrStatusFn = opts.getPrStatusFn || getPrStatus;
|
|
119
|
+
const findStaleMissionWorktreesFn = opts.findStaleMissionWorktreesFn || findStaleMissionWorktrees;
|
|
120
|
+
const readAgentConfigOrExitFn = opts.readAgentConfigOrExitFn || readAgentConfigOrExit;
|
|
121
|
+
const eligibleAgentsForStepFn = opts.eligibleAgentsForStepFn || eligibleAgentsForStep;
|
|
122
|
+
const allWorkflowAgentNamesFn = opts.allWorkflowAgentNamesFn || (() => WORKFLOW_AGENT_NAMES);
|
|
123
|
+
const workflowLauncherStatusFn = opts.workflowLauncherStatusFn || workflowLauncherStatus;
|
|
124
|
+
const getLastThreeCommitsFn = opts.getLastThreeCommitsFn || getLastThreeCommits;
|
|
125
|
+
const getUncommittedCountFn = opts.getUncommittedCountFn || getUncommittedCount;
|
|
126
|
+
const detectRebaseStateFn = opts.detectRebaseStateFn || detectRebaseState;
|
|
122
127
|
|
|
123
128
|
const explicitSlug = args[0];
|
|
124
129
|
const slug = inferSlugFn(explicitSlug);
|
|
@@ -168,17 +173,17 @@ function status(args, options = {}) {
|
|
|
168
173
|
|
|
169
174
|
if (!explicitSlug) {
|
|
170
175
|
const staleWorktrees = findStaleMissionWorktreesFn();
|
|
171
|
-
staleWorktrees.forEach(entry => {
|
|
176
|
+
staleWorktrees.forEach(/** @param {{path: string, branch: string | null, taskStatus: string | null, cleanupCommand?: string}} entry */ (entry) => {
|
|
172
177
|
log(`Stale worktree: ${fmt.path(entry.path)} (task: ${entry.taskStatus})`);
|
|
173
178
|
try {
|
|
174
179
|
const rebaseState = detectRebaseStateFn(entry.path);
|
|
175
180
|
if (rebaseState.inProgress) {
|
|
176
|
-
logRebaseDiagnostics(log, `Rebase in progress on ${formatWorktreeBranch(entry.branch)}`, rebaseState);
|
|
181
|
+
logRebaseDiagnostics(log, `Rebase in progress on ${formatWorktreeBranch(entry.branch || '')}`, rebaseState);
|
|
177
182
|
}
|
|
178
183
|
} catch (_) {
|
|
179
184
|
// Ignore stale worktrees that disappear during inspection.
|
|
180
185
|
}
|
|
181
|
-
log(` Cleanup: ${fmt.command(entry.cleanupCommand)}`);
|
|
186
|
+
log(` Cleanup: ${fmt.command(entry.cleanupCommand || '')}`);
|
|
182
187
|
});
|
|
183
188
|
}
|
|
184
189
|
|
|
@@ -203,7 +208,7 @@ function status(args, options = {}) {
|
|
|
203
208
|
|
|
204
209
|
const lastThree = getLastThreeCommitsFn();
|
|
205
210
|
log('Last 3 commits:');
|
|
206
|
-
lastThree.forEach(c => log(` - ${c}`));
|
|
211
|
+
lastThree.forEach(/** @param {string} c */ (c) => log(` - ${c}`));
|
|
207
212
|
|
|
208
213
|
log(`Uncommitted files: ${getUncommittedCountFn()}`);
|
|
209
214
|
log(fmt.bold('----------------------'));
|
package/lib/core/fmt.js
CHANGED
|
@@ -34,18 +34,22 @@ const agentMap = {
|
|
|
34
34
|
custom: 'yellow',
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
+
/** @param {import('node:util').InspectColor} format @param {string} text */
|
|
37
38
|
function colorize(format, text) {
|
|
38
39
|
return styleText(format, String(text ?? ''));
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
/** @param {string} text */
|
|
41
43
|
function stripAnsi(text) {
|
|
42
44
|
return String(text ?? '').replace(/\x1B\[[0-9;]*m/g, '');
|
|
43
45
|
}
|
|
44
46
|
|
|
47
|
+
/** @param {string} text */
|
|
45
48
|
function visibleWidth(text) {
|
|
46
49
|
return stripAnsi(text).length;
|
|
47
50
|
}
|
|
48
51
|
|
|
52
|
+
/** @param {string} text @param {number} width */
|
|
49
53
|
function padVisibleEnd(text, width) {
|
|
50
54
|
const stringValue = String(text ?? '');
|
|
51
55
|
const padLength = Math.max(0, width - visibleWidth(stringValue));
|
|
@@ -55,15 +59,19 @@ function padVisibleEnd(text, width) {
|
|
|
55
59
|
/**
|
|
56
60
|
* Formats a status line prefix (e.g. "[PASS]").
|
|
57
61
|
*/
|
|
62
|
+
/** @param {string} type @param {string} text */
|
|
58
63
|
function status(type, text) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
64
|
+
/** @type {Record<string,string>} */
|
|
65
|
+
const sm = statusMap;
|
|
66
|
+
const format = sm[type];
|
|
67
|
+
if (!format) {return `[${type}] ${text}`;}
|
|
68
|
+
return `${colorize(/** @type {import('node:util').InspectColor} */(format), `[${type}]`)} ${text}`;
|
|
62
69
|
}
|
|
63
70
|
|
|
64
71
|
/**
|
|
65
72
|
* Formats an agent family name.
|
|
66
73
|
*/
|
|
74
|
+
/** @param {string} family @param {string} [text] */
|
|
67
75
|
function agent(family, text = family) {
|
|
68
76
|
let label = text;
|
|
69
77
|
// Disambiguate the opencode-backed `custom` family only when the caller
|
|
@@ -73,15 +81,19 @@ function agent(family, text = family) {
|
|
|
73
81
|
if (family === 'custom' && text !== 'custom') {
|
|
74
82
|
label = 'custom (opencode)';
|
|
75
83
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
84
|
+
/** @type {Record<string,string>} */
|
|
85
|
+
const am = agentMap;
|
|
86
|
+
const format = am[family];
|
|
87
|
+
if (!format) {return label;}
|
|
88
|
+
return colorize(/** @type {import('node:util').InspectColor} */(format), label);
|
|
79
89
|
}
|
|
80
90
|
|
|
91
|
+
/** @param {string} text */
|
|
81
92
|
function bold(text) {
|
|
82
93
|
return colorize('bold', text);
|
|
83
94
|
}
|
|
84
95
|
|
|
96
|
+
/** @param {string} text */
|
|
85
97
|
function dim(text) {
|
|
86
98
|
return colorize('gray', text);
|
|
87
99
|
}
|
|
@@ -89,6 +101,7 @@ function dim(text) {
|
|
|
89
101
|
/**
|
|
90
102
|
* Aligned key-value pair.
|
|
91
103
|
*/
|
|
104
|
+
/** @param {string} key @param {string} value @param {number} width */
|
|
92
105
|
function kv(key, value, width = 30) {
|
|
93
106
|
return `${bold(key.padEnd(width))} ${value}`;
|
|
94
107
|
}
|
|
@@ -96,93 +109,123 @@ function kv(key, value, width = 30) {
|
|
|
96
109
|
/**
|
|
97
110
|
* Aligned table of columns.
|
|
98
111
|
*/
|
|
112
|
+
/** @param {string[][]} rows @param {{indent?: number, colPadding?: number}} [opts] */
|
|
99
113
|
function table(rows, { indent = 2, colPadding = 2 } = {}) {
|
|
100
|
-
if (rows.length === 0) return '';
|
|
114
|
+
if (rows.length === 0) {return '';}
|
|
115
|
+
/** @param {unknown} _ @param {number} i */
|
|
101
116
|
const colWidths = rows[0].map((_, i) => Math.max(...rows.map(row => visibleWidth(row[i] ?? ''))));
|
|
102
117
|
const padding = ' '.repeat(indent);
|
|
118
|
+
/** @param {string[]} row */
|
|
103
119
|
return rows.map(row => padding + row.map((col, i) => padVisibleEnd(col ?? '', colWidths[i] + colPadding)).join('')).join('\n');
|
|
104
120
|
}
|
|
105
121
|
|
|
106
122
|
/**
|
|
107
123
|
* Bulleted list.
|
|
108
124
|
*/
|
|
125
|
+
/** @param {string[]} items @param {{bullet?: string, indent?: number}} [opts] */
|
|
109
126
|
function list(items, { bullet = '-', indent = 2 } = {}) {
|
|
110
127
|
const padding = ' '.repeat(indent);
|
|
128
|
+
/** @param {string} item */
|
|
111
129
|
return items.map(item => `${padding}${bullet} ${item}`).join('\n');
|
|
112
130
|
}
|
|
113
131
|
|
|
114
132
|
const semantic = {
|
|
115
|
-
path: (text) => colorize('blue', text),
|
|
116
|
-
slug: (text) => colorize('cyan', bold(text)),
|
|
117
|
-
branch: (text) => colorize('magenta', text),
|
|
118
|
-
sha: (text) => colorize('yellow', text),
|
|
119
|
-
command: (text) => colorize('green', text),
|
|
133
|
+
path: /** @param {string} text */(text) => colorize('blue', text),
|
|
134
|
+
slug: /** @param {string} text */(text) => colorize('cyan', bold(text)),
|
|
135
|
+
branch: /** @param {string} text */(text) => colorize('magenta', text),
|
|
136
|
+
sha: /** @param {string} text */(text) => colorize('yellow', text),
|
|
137
|
+
command: /** @param {string} text */(text) => colorize('green', text),
|
|
120
138
|
};
|
|
121
139
|
|
|
140
|
+
/** @type {{log: Function, error: Function}} */
|
|
122
141
|
let currentLogger = {
|
|
142
|
+
/** @param {...any} args */
|
|
123
143
|
log: (...args) => console.log(...args),
|
|
144
|
+
/** @param {...any} args */
|
|
124
145
|
error: (...args) => console.error(...args),
|
|
125
146
|
};
|
|
126
147
|
|
|
148
|
+
/** @typedef {{log?: Function, error?: Function}} LoggerObj */
|
|
149
|
+
|
|
150
|
+
/** @type{{info: Function, pass: Function, fail: Function, warn: Function, error: Function, debug: Function, plain: Function, plainError: Function}} */
|
|
127
151
|
const log = {
|
|
152
|
+
/** @param {string} text */
|
|
128
153
|
info: (text) => {
|
|
154
|
+
/** @param {string} line */
|
|
129
155
|
return text.toString().split('\n').map(line => {
|
|
130
156
|
const s = status('INFO', line);
|
|
131
157
|
currentLogger.log(s);
|
|
132
158
|
return s;
|
|
133
159
|
}).join('\n');
|
|
134
160
|
},
|
|
161
|
+
/** @param {string} text */
|
|
135
162
|
pass: (text) => {
|
|
163
|
+
/** @param {string} line */
|
|
136
164
|
return text.toString().split('\n').map(line => {
|
|
137
165
|
const s = status('PASS', line);
|
|
138
166
|
currentLogger.log(s);
|
|
139
167
|
return s;
|
|
140
168
|
}).join('\n');
|
|
141
169
|
},
|
|
170
|
+
/** @param {string} text */
|
|
142
171
|
fail: (text) => {
|
|
172
|
+
/** @param {string} line */
|
|
143
173
|
return text.toString().split('\n').map(line => {
|
|
144
174
|
const s = status('FAIL', line);
|
|
145
175
|
currentLogger.error(s);
|
|
146
176
|
return s;
|
|
147
177
|
}).join('\n');
|
|
148
178
|
},
|
|
179
|
+
/** @param {string} text */
|
|
149
180
|
warn: (text) => {
|
|
181
|
+
/** @param {string} line */
|
|
150
182
|
return text.toString().split('\n').map(line => {
|
|
151
183
|
const s = status('WARN', line);
|
|
152
184
|
currentLogger.log(s);
|
|
153
185
|
return s;
|
|
154
186
|
}).join('\n');
|
|
155
187
|
},
|
|
188
|
+
/** @param {string} text */
|
|
156
189
|
error: (text) => {
|
|
190
|
+
/** @param {string} line */
|
|
157
191
|
return text.toString().split('\n').map(line => {
|
|
158
192
|
const s = status('FAIL', line);
|
|
159
193
|
currentLogger.error(s);
|
|
160
194
|
return s;
|
|
161
195
|
}).join('\n');
|
|
162
196
|
},
|
|
197
|
+
/** @param {string} text */
|
|
163
198
|
debug: (text) => {
|
|
164
|
-
if (!process.env.DEBUG) return null;
|
|
199
|
+
if (!process.env.DEBUG) {return null;}
|
|
200
|
+
/** @param {string} line */
|
|
165
201
|
return text.toString().split('\n').map(line => {
|
|
166
202
|
const s = status('DEBUG', line);
|
|
167
203
|
currentLogger.log(s);
|
|
168
204
|
return s;
|
|
169
205
|
}).join('\n');
|
|
170
206
|
},
|
|
207
|
+
/** @param {string} text */
|
|
171
208
|
plain: (text) => {
|
|
209
|
+
/** @param {string} line */
|
|
172
210
|
text.toString().split('\n').forEach(line => currentLogger.log(line));
|
|
173
211
|
return text;
|
|
174
212
|
},
|
|
213
|
+
/** @param {string} text */
|
|
175
214
|
plainError: (text) => {
|
|
215
|
+
/** @param {string} line */
|
|
176
216
|
text.toString().split('\n').forEach(line => currentLogger.error(line));
|
|
177
217
|
return text;
|
|
178
218
|
},
|
|
179
219
|
};
|
|
180
220
|
|
|
221
|
+
/** @param {LoggerObj|Function} newLogger */
|
|
181
222
|
function setLogger(newLogger) {
|
|
182
223
|
const old = currentLogger;
|
|
224
|
+
/** @type {LoggerObj} */
|
|
225
|
+
const logger = /** @type {LoggerObj} */(newLogger);
|
|
183
226
|
currentLogger = {
|
|
184
|
-
log:
|
|
185
|
-
error:
|
|
227
|
+
log: typeof logger.log === 'function' ? logger.log : (() => {}),
|
|
228
|
+
error: typeof logger.error === 'function' ? logger.error : (typeof logger.log === 'function' ? logger.log : (() => {})),
|
|
186
229
|
};
|
|
187
230
|
return old;
|
|
188
231
|
}
|
package/lib/core/git.js
CHANGED
|
@@ -1,26 +1,28 @@
|
|
|
1
1
|
const childProcess = require('child_process');
|
|
2
2
|
|
|
3
|
+
/** @param {string[]} args @param {object} options */
|
|
3
4
|
function git(args, options = {}) {
|
|
4
5
|
const spawnOptions = {
|
|
5
6
|
encoding: 'utf8',
|
|
6
7
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
7
8
|
...options
|
|
8
9
|
};
|
|
9
|
-
const result = childProcess.spawnSync('git', args, spawnOptions);
|
|
10
|
-
if (result.error && result.status
|
|
10
|
+
const result = childProcess.spawnSync('git', args, /** @type {import('child_process').SpawnSyncOptionsWithStringEncoding} */(spawnOptions));
|
|
11
|
+
if (result.error && result.status === null) {
|
|
11
12
|
throw result.error;
|
|
12
13
|
}
|
|
13
14
|
return result;
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
/** @param {string} command @param {string[]} args @param {object} options */
|
|
16
18
|
function run(command, args, options = {}) {
|
|
17
19
|
const spawnOptions = {
|
|
18
20
|
encoding: 'utf8',
|
|
19
21
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
20
22
|
...options
|
|
21
23
|
};
|
|
22
|
-
const result = childProcess.spawnSync(command, args, spawnOptions);
|
|
23
|
-
if (result.error && result.status
|
|
24
|
+
const result = childProcess.spawnSync(command, args, /** @type {import('child_process').SpawnSyncOptionsWithStringEncoding} */(spawnOptions));
|
|
25
|
+
if (result.error && result.status === null) {
|
|
24
26
|
throw result.error;
|
|
25
27
|
}
|
|
26
28
|
return result;
|
|
@@ -46,7 +48,7 @@ function isDirty(cwd = process.cwd()) {
|
|
|
46
48
|
|
|
47
49
|
function getUncommittedCount(cwd = process.cwd()) {
|
|
48
50
|
const result = git(['-C', cwd, 'status', '--porcelain']);
|
|
49
|
-
if (!result.stdout.trim()) return 0;
|
|
51
|
+
if (!result.stdout.trim()) {return 0;}
|
|
50
52
|
return result.stdout.trim().split('\n').length;
|
|
51
53
|
}
|
|
52
54
|
|
package/lib/core/gitignore.js
CHANGED
|
@@ -77,16 +77,18 @@ function ensureWorkflowGitignore(rootDir, options = {}) {
|
|
|
77
77
|
try {
|
|
78
78
|
existingContent = readFileSyncFn(gitignorePath, 'utf8');
|
|
79
79
|
} catch (err) {
|
|
80
|
-
logFn(`[gitignore] Could not read ${gitignorePath}: ${err.message}`);
|
|
81
|
-
return { ok: false, created: false, appended: 0, skipped: false, reason: `read-error: ${err.message}` };
|
|
80
|
+
logFn(`[gitignore] Could not read ${gitignorePath}: ${/** @type{Error} */(err).message}`);
|
|
81
|
+
return { ok: false, created: false, appended: 0, skipped: false, reason: `read-error: ${/** @type{Error} */(err).message}` };
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
// Parse existing entries
|
|
85
85
|
const existingLines = existingContent.split(/\r?\n/);
|
|
86
86
|
const existingEntries = new Set(
|
|
87
87
|
existingLines
|
|
88
|
-
|
|
89
|
-
.
|
|
88
|
+
/** @param {string} line */
|
|
89
|
+
.map(/** @param {string} line */(line) => line.trim())
|
|
90
|
+
/** @param {string} line */
|
|
91
|
+
.filter(/** @param {string} line */(line) => line.length > 0 && !line.startsWith('#'))
|
|
90
92
|
);
|
|
91
93
|
|
|
92
94
|
// Find missing entries
|