@magnusekdahl/parallix 1.2.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/agents/agents.js +737 -838
- package/lib/agents/agents.ts +930 -0
- package/lib/agents/claude-telemetry.js +149 -154
- package/lib/agents/claude-telemetry.ts +233 -0
- package/lib/agents/claude.js +121 -131
- package/lib/agents/claude.ts +156 -0
- package/lib/agents/codex-telemetry.js +176 -164
- package/lib/agents/codex-telemetry.ts +205 -0
- package/lib/agents/codex.js +168 -201
- package/lib/agents/codex.ts +236 -0
- package/lib/agents/limit-hit.js +216 -226
- package/lib/agents/limit-hit.ts +262 -0
- package/lib/agents/mistral-telemetry.js +7 -10
- package/lib/agents/mistral-telemetry.ts +44 -0
- package/lib/agents/mistral.js +40 -58
- package/lib/agents/mistral.ts +84 -0
- package/lib/agents/opencode-export.js +126 -127
- package/lib/agents/opencode-export.ts +160 -0
- package/lib/agents/opencode-telemetry.js +282 -261
- package/lib/agents/opencode-telemetry.ts +350 -0
- package/lib/agents/opencode.js +263 -305
- package/lib/agents/opencode.ts +370 -0
- package/lib/agents/stage-telemetry.js +23 -24
- package/lib/agents/stage-telemetry.ts +47 -0
- package/lib/commands/integrate.js +9 -3
- package/lib/core/fmt.js +140 -211
- package/lib/core/fmt.ts +189 -0
- package/lib/core/git.js +121 -102
- package/lib/core/git.ts +146 -0
- package/lib/core/gitignore.js +59 -106
- package/lib/core/gitignore.ts +103 -0
- package/lib/core/mission-utils.js +833 -875
- package/lib/core/mission-utils.ts +998 -0
- package/lib/core/persistent-data-migration.js +247 -205
- package/lib/core/persistent-data-migration.ts +255 -0
- package/lib/core/product-config.js +400 -476
- package/lib/core/product-config.ts +518 -0
- package/lib/core/runtime-matrix.js +41 -87
- package/lib/core/runtime-matrix.ts +84 -0
- package/lib/core/spawn-tee.js +152 -192
- package/lib/core/spawn-tee.ts +202 -0
- package/lib/core/state-map.js +92 -92
- package/lib/core/state-map.ts +115 -0
- package/lib/core/storage.js +101 -129
- package/lib/core/storage.ts +182 -0
- package/lib/core/verification.js +148 -138
- package/lib/core/verification.ts +200 -0
- package/package.json +4 -2
- package/prompts/draft.md +1 -0
- package/prompts/portfolio.md +1 -1
- package/px.js +0 -2
package/lib/core/git.js
CHANGED
|
@@ -1,121 +1,140 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.git = git;
|
|
37
|
+
exports.run = run;
|
|
38
|
+
exports.getCurrentBranch = getCurrentBranch;
|
|
39
|
+
exports.getWorktreeStatus = getWorktreeStatus;
|
|
40
|
+
exports.isDirty = isDirty;
|
|
41
|
+
exports.getUncommittedCount = getUncommittedCount;
|
|
42
|
+
exports.parseUnmergedFiles = parseUnmergedFiles;
|
|
43
|
+
exports.detectRebaseState = detectRebaseState;
|
|
44
|
+
exports.getLastCommit = getLastCommit;
|
|
45
|
+
exports.getLastThreeCommits = getLastThreeCommits;
|
|
46
|
+
const node_child_process_1 = require("node:child_process");
|
|
47
|
+
const fsMod = __importStar(require("node:fs"));
|
|
48
|
+
const pathMod = __importStar(require("node:path"));
|
|
4
49
|
function git(args, options = {}) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
50
|
+
const spawnOptions = {
|
|
51
|
+
encoding: 'utf8',
|
|
52
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
53
|
+
...options
|
|
54
|
+
};
|
|
55
|
+
const result = (0, node_child_process_1.spawnSync)('git', args, spawnOptions);
|
|
56
|
+
if (result.error && result.status === null) {
|
|
57
|
+
throw result.error;
|
|
58
|
+
}
|
|
59
|
+
return { status: result.status, signal: result.signal, stdout: String(result.stdout ?? ''), stderr: String(result.stderr ?? ''), error: result.error };
|
|
15
60
|
}
|
|
16
|
-
|
|
17
|
-
/** @param {string} command @param {string[]} args @param {object} options */
|
|
18
61
|
function run(command, args, options = {}) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
62
|
+
const spawnOptions = {
|
|
63
|
+
encoding: 'utf8',
|
|
64
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
65
|
+
...options
|
|
66
|
+
};
|
|
67
|
+
const result = (0, node_child_process_1.spawnSync)(command, args, spawnOptions);
|
|
68
|
+
if (result.error && result.status === null) {
|
|
69
|
+
throw result.error;
|
|
70
|
+
}
|
|
71
|
+
return { status: result.status, signal: result.signal, stdout: String(result.stdout ?? ''), stderr: String(result.stderr ?? ''), error: result.error };
|
|
29
72
|
}
|
|
30
|
-
|
|
31
73
|
function getCurrentBranch(cwd = process.cwd()) {
|
|
32
|
-
|
|
33
|
-
|
|
74
|
+
const result = git(['-C', cwd, 'branch', '--show-current']);
|
|
75
|
+
return result.stdout.trim();
|
|
34
76
|
}
|
|
35
|
-
|
|
36
77
|
function getWorktreeStatus(cwd = process.cwd()) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
78
|
+
const result = git(['-C', cwd, 'status', '--porcelain']);
|
|
79
|
+
return result.stdout
|
|
80
|
+
.split('\n')
|
|
81
|
+
.map(line => line.trimEnd())
|
|
82
|
+
.filter(Boolean);
|
|
42
83
|
}
|
|
43
|
-
|
|
44
84
|
function isDirty(cwd = process.cwd()) {
|
|
45
|
-
|
|
46
|
-
|
|
85
|
+
const result = git(['-C', cwd, 'status', '--porcelain']);
|
|
86
|
+
return result.stdout.trim().length > 0;
|
|
47
87
|
}
|
|
48
|
-
|
|
49
88
|
function getUncommittedCount(cwd = process.cwd()) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
89
|
+
const result = git(['-C', cwd, 'status', '--porcelain']);
|
|
90
|
+
if (!result.stdout.trim()) {
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
return result.stdout.trim().split('\n').length;
|
|
53
94
|
}
|
|
54
|
-
|
|
55
95
|
function parseUnmergedFiles(output = '') {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
.filter(Boolean)
|
|
63
|
-
));
|
|
96
|
+
return Array.from(new Set(output
|
|
97
|
+
.split('\n')
|
|
98
|
+
.map(line => line.trim())
|
|
99
|
+
.filter(Boolean)
|
|
100
|
+
.map(line => line.split('\t')[1])
|
|
101
|
+
.filter(Boolean)));
|
|
64
102
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
rebaseHead,
|
|
94
|
-
detached,
|
|
95
|
-
unmergedFiles,
|
|
96
|
-
rebaseDir
|
|
97
|
-
};
|
|
103
|
+
function detectRebaseState(cwd = process.cwd(), options = {}) {
|
|
104
|
+
const { gitRunner = git, fsModule = fsMod, pathModule = pathMod } = options;
|
|
105
|
+
const gitDirResult = gitRunner(['-C', cwd, 'rev-parse', '--git-dir']);
|
|
106
|
+
const resolvedGitDir = gitDirResult.status === 0
|
|
107
|
+
? gitDirResult.stdout.trim()
|
|
108
|
+
: '.git';
|
|
109
|
+
const gitDir = pathModule.isAbsolute(resolvedGitDir)
|
|
110
|
+
? resolvedGitDir
|
|
111
|
+
: pathModule.join(cwd, resolvedGitDir);
|
|
112
|
+
const rebaseMergeDir = pathModule.join(gitDir, 'rebase-merge');
|
|
113
|
+
const rebaseApplyDir = pathModule.join(gitDir, 'rebase-apply');
|
|
114
|
+
const rebaseDir = fsModule.existsSync(rebaseMergeDir)
|
|
115
|
+
? rebaseMergeDir
|
|
116
|
+
: (fsModule.existsSync(rebaseApplyDir) ? rebaseApplyDir : null);
|
|
117
|
+
const headResult = gitRunner(['-C', cwd, 'symbolic-ref', '--quiet', '--short', 'HEAD']);
|
|
118
|
+
const detached = headResult.status !== 0;
|
|
119
|
+
const showCurrentResult = gitRunner(['-C', cwd, 'rebase', '--show-current']);
|
|
120
|
+
const rebaseHead = showCurrentResult.status === 0 ? showCurrentResult.stdout.trim() : '';
|
|
121
|
+
const unmergedResult = gitRunner(['-C', cwd, 'ls-files', '-u']);
|
|
122
|
+
const unmergedFiles = unmergedResult.status === 0 ? parseUnmergedFiles(unmergedResult.stdout) : [];
|
|
123
|
+
const inProgress = Boolean(rebaseDir || rebaseHead || unmergedFiles.length > 0);
|
|
124
|
+
return {
|
|
125
|
+
inProgress,
|
|
126
|
+
rebaseHead,
|
|
127
|
+
detached,
|
|
128
|
+
unmergedFiles,
|
|
129
|
+
rebaseDir
|
|
130
|
+
};
|
|
98
131
|
}
|
|
99
|
-
|
|
100
132
|
function getLastCommit() {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
133
|
+
const result = git(['log', '-1', '--format=%H|%ad|%s']);
|
|
134
|
+
const [sha, date, subject] = result.stdout.trim().split('|');
|
|
135
|
+
return { sha, date, subject };
|
|
104
136
|
}
|
|
105
|
-
|
|
106
137
|
function getLastThreeCommits() {
|
|
107
|
-
|
|
108
|
-
|
|
138
|
+
const result = git(['log', '-3', '--format=%s']);
|
|
139
|
+
return result.stdout.trim().split('\n');
|
|
109
140
|
}
|
|
110
|
-
|
|
111
|
-
module.exports = {
|
|
112
|
-
git,
|
|
113
|
-
run,
|
|
114
|
-
getCurrentBranch,
|
|
115
|
-
getWorktreeStatus,
|
|
116
|
-
isDirty,
|
|
117
|
-
getUncommittedCount,
|
|
118
|
-
detectRebaseState,
|
|
119
|
-
getLastCommit,
|
|
120
|
-
getLastThreeCommits
|
|
121
|
-
};
|
package/lib/core/git.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import childProcess, { spawnSync } from 'node:child_process';
|
|
2
|
+
import type { SpawnSyncOptions } from 'node:child_process';
|
|
3
|
+
import * as fsMod from 'node:fs';
|
|
4
|
+
import * as pathMod from 'node:path';
|
|
5
|
+
|
|
6
|
+
interface GitOptions {
|
|
7
|
+
encoding?: BufferEncoding;
|
|
8
|
+
stdio?: import('node:child_process').StdioOptions;
|
|
9
|
+
cwd?: string;
|
|
10
|
+
env?: NodeJS.ProcessEnv;
|
|
11
|
+
maxBuffer?: number;
|
|
12
|
+
[key: string]: unknown;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface GitResult {
|
|
16
|
+
status: number | null;
|
|
17
|
+
signal: string | null;
|
|
18
|
+
stdout: string;
|
|
19
|
+
stderr: string;
|
|
20
|
+
error?: Error | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function git(args: string[], options: GitOptions = {}): GitResult {
|
|
24
|
+
const spawnOptions: SpawnSyncOptions = {
|
|
25
|
+
encoding: 'utf8',
|
|
26
|
+
stdio: ['ignore', 'pipe', 'pipe'] as const,
|
|
27
|
+
...options
|
|
28
|
+
};
|
|
29
|
+
const result = spawnSync('git', args, spawnOptions);
|
|
30
|
+
if (result.error && result.status === null) {
|
|
31
|
+
throw result.error;
|
|
32
|
+
}
|
|
33
|
+
return { status: result.status, signal: result.signal, stdout: String(result.stdout ?? ''), stderr: String(result.stderr ?? ''), error: result.error };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function run(command: string, args: string[], options: GitOptions = {}): GitResult {
|
|
37
|
+
const spawnOptions: SpawnSyncOptions = {
|
|
38
|
+
encoding: 'utf8',
|
|
39
|
+
stdio: ['ignore', 'pipe', 'pipe'] as const,
|
|
40
|
+
...options
|
|
41
|
+
};
|
|
42
|
+
const result = spawnSync(command, args, spawnOptions);
|
|
43
|
+
if (result.error && result.status === null) {
|
|
44
|
+
throw result.error;
|
|
45
|
+
}
|
|
46
|
+
return { status: result.status, signal: result.signal, stdout: String(result.stdout ?? ''), stderr: String(result.stderr ?? ''), error: result.error };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function getCurrentBranch(cwd: string = process.cwd()): string {
|
|
50
|
+
const result = git(['-C', cwd, 'branch', '--show-current']);
|
|
51
|
+
return result.stdout.trim();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function getWorktreeStatus(cwd: string = process.cwd()): string[] {
|
|
55
|
+
const result = git(['-C', cwd, 'status', '--porcelain']);
|
|
56
|
+
return result.stdout
|
|
57
|
+
.split('\n')
|
|
58
|
+
.map(line => line.trimEnd())
|
|
59
|
+
.filter(Boolean);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function isDirty(cwd: string = process.cwd()): boolean {
|
|
63
|
+
const result = git(['-C', cwd, 'status', '--porcelain']);
|
|
64
|
+
return result.stdout.trim().length > 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function getUncommittedCount(cwd: string = process.cwd()): number {
|
|
68
|
+
const result = git(['-C', cwd, 'status', '--porcelain']);
|
|
69
|
+
if (!result.stdout.trim()) {return 0;}
|
|
70
|
+
return result.stdout.trim().split('\n').length;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function parseUnmergedFiles(output: string = ''): string[] {
|
|
74
|
+
return Array.from(new Set(
|
|
75
|
+
output
|
|
76
|
+
.split('\n')
|
|
77
|
+
.map(line => line.trim())
|
|
78
|
+
.filter(Boolean)
|
|
79
|
+
.map(line => line.split('\t')[1])
|
|
80
|
+
.filter(Boolean)
|
|
81
|
+
));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface RebaseStateResult {
|
|
85
|
+
inProgress: boolean;
|
|
86
|
+
rebaseHead: string;
|
|
87
|
+
detached: boolean;
|
|
88
|
+
unmergedFiles: string[];
|
|
89
|
+
rebaseDir: string | null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
interface GitRunner {
|
|
93
|
+
(args: string[]): GitResult;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface DetectRebaseOptions {
|
|
97
|
+
gitRunner?: GitRunner;
|
|
98
|
+
fsModule?: typeof fsMod;
|
|
99
|
+
pathModule?: typeof pathMod;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function detectRebaseState(cwd: string = process.cwd(), options: DetectRebaseOptions = {}): RebaseStateResult {
|
|
103
|
+
const { gitRunner = git, fsModule = fsMod, pathModule = pathMod } = options;
|
|
104
|
+
const gitDirResult = gitRunner(['-C', cwd, 'rev-parse', '--git-dir']);
|
|
105
|
+
const resolvedGitDir = gitDirResult.status === 0
|
|
106
|
+
? gitDirResult.stdout.trim()
|
|
107
|
+
: '.git';
|
|
108
|
+
const gitDir = pathModule.isAbsolute(resolvedGitDir)
|
|
109
|
+
? resolvedGitDir
|
|
110
|
+
: pathModule.join(cwd, resolvedGitDir);
|
|
111
|
+
const rebaseMergeDir = pathModule.join(gitDir, 'rebase-merge');
|
|
112
|
+
const rebaseApplyDir = pathModule.join(gitDir, 'rebase-apply');
|
|
113
|
+
const rebaseDir = fsModule.existsSync(rebaseMergeDir)
|
|
114
|
+
? rebaseMergeDir
|
|
115
|
+
: (fsModule.existsSync(rebaseApplyDir) ? rebaseApplyDir : null);
|
|
116
|
+
|
|
117
|
+
const headResult = gitRunner(['-C', cwd, 'symbolic-ref', '--quiet', '--short', 'HEAD']);
|
|
118
|
+
const detached = headResult.status !== 0;
|
|
119
|
+
|
|
120
|
+
const showCurrentResult = gitRunner(['-C', cwd, 'rebase', '--show-current']);
|
|
121
|
+
const rebaseHead = showCurrentResult.status === 0 ? showCurrentResult.stdout.trim() : '';
|
|
122
|
+
|
|
123
|
+
const unmergedResult = gitRunner(['-C', cwd, 'ls-files', '-u']);
|
|
124
|
+
const unmergedFiles = unmergedResult.status === 0 ? parseUnmergedFiles(unmergedResult.stdout) : [];
|
|
125
|
+
|
|
126
|
+
const inProgress = Boolean(rebaseDir || rebaseHead || unmergedFiles.length > 0);
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
inProgress,
|
|
130
|
+
rebaseHead,
|
|
131
|
+
detached,
|
|
132
|
+
unmergedFiles,
|
|
133
|
+
rebaseDir
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function getLastCommit(): { sha: string; date: string; subject: string } {
|
|
138
|
+
const result = git(['log', '-1', '--format=%H|%ad|%s']);
|
|
139
|
+
const [sha, date, subject] = result.stdout.trim().split('|');
|
|
140
|
+
return { sha, date, subject };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function getLastThreeCommits(): string[] {
|
|
144
|
+
const result = git(['log', '-3', '--format=%s']);
|
|
145
|
+
return result.stdout.trim().split('\n');
|
|
146
|
+
}
|
package/lib/core/gitignore.js
CHANGED
|
@@ -1,112 +1,65 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
* committed to version control.
|
|
8
|
-
*/
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
6
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
7
|
const WORKFLOW_ENTRIES = [
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
'.workflow/',
|
|
9
|
+
'.sessions/',
|
|
10
|
+
'.forgejo-local/',
|
|
11
|
+
'workflow/.cache/',
|
|
12
|
+
'workflow/.sessions/',
|
|
13
|
+
'workflow/config/agents.local.json',
|
|
14
|
+
'agents.local.json',
|
|
17
15
|
];
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Ensure that a repository's .gitignore contains all workflow-generated paths.
|
|
21
|
-
*
|
|
22
|
-
* Handles:
|
|
23
|
-
* - Missing .gitignore: creates one with all 7 entries
|
|
24
|
-
* - Existing .gitignore: appends only missing entries, skips duplicates
|
|
25
|
-
* - Symlinked .gitignore: skips modification, logs warning
|
|
26
|
-
* - Non-git directories: skips gracefully without crashing
|
|
27
|
-
*
|
|
28
|
-
* @param {string} rootDir - Path to the repository root
|
|
29
|
-
* @param {object} options - Injected dependencies
|
|
30
|
-
* @param {Function} [options.existsSyncFn] - fs.existsSync replacement
|
|
31
|
-
* @param {Function} [options.lstatSyncFn] - fs.lstatSync replacement
|
|
32
|
-
* @param {Function} [options.readFileSyncFn] - fs.readFileSync replacement
|
|
33
|
-
* @param {Function} [options.writeFileSyncFn] - fs.writeFileSync replacement
|
|
34
|
-
* @param {Function} [options.logFn] - Logger function
|
|
35
|
-
* @returns {{ ok: boolean, created: boolean, appended: number, skipped: boolean, reason?: string }}
|
|
36
|
-
*/
|
|
37
16
|
function ensureWorkflowGitignore(rootDir, options = {}) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
} = options;
|
|
45
|
-
|
|
46
|
-
const gitignorePath = path.join(rootDir, '.gitignore');
|
|
47
|
-
const gitDirPath = path.join(rootDir, '.git');
|
|
48
|
-
|
|
49
|
-
// Check if this is a git repository
|
|
50
|
-
if (!existsSyncFn(gitDirPath)) {
|
|
51
|
-
logFn(`[gitignore] Skipping ${rootDir}: not a git repository (no .git directory)`);
|
|
52
|
-
return { ok: true, created: false, appended: 0, skipped: true, reason: 'not-a-git-repo' };
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Check if .gitignore exists
|
|
56
|
-
if (!existsSyncFn(gitignorePath)) {
|
|
57
|
-
// Create new .gitignore with all entries
|
|
58
|
-
const content = WORKFLOW_ENTRIES.join('\n') + '\n';
|
|
59
|
-
writeFileSyncFn(gitignorePath, content, 'utf8');
|
|
60
|
-
logFn(`[gitignore] Created ${gitignorePath} with ${WORKFLOW_ENTRIES.length} entries`);
|
|
61
|
-
return { ok: true, created: true, appended: WORKFLOW_ENTRIES.length, skipped: false };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// .gitignore exists - check for symlink
|
|
65
|
-
try {
|
|
66
|
-
const stat = lstatSyncFn(gitignorePath);
|
|
67
|
-
if (stat.isSymbolicLink()) {
|
|
68
|
-
logFn(`[gitignore] Skipping ${gitignorePath}: symbolic link, not modifying`);
|
|
69
|
-
return { ok: true, created: false, appended: 0, skipped: true, reason: 'symlink' };
|
|
17
|
+
const { existsSyncFn = node_fs_1.default.existsSync, lstatSyncFn = node_fs_1.default.lstatSync, readFileSyncFn = node_fs_1.default.readFileSync, writeFileSyncFn = node_fs_1.default.writeFileSync, logFn = () => { }, } = options;
|
|
18
|
+
const gitignorePath = node_path_1.default.join(rootDir, '.gitignore');
|
|
19
|
+
const gitDirPath = node_path_1.default.join(rootDir, '.git');
|
|
20
|
+
if (!existsSyncFn(gitDirPath)) {
|
|
21
|
+
logFn(`[gitignore] Skipping ${rootDir}: not a git repository (no .git directory)`);
|
|
22
|
+
return { ok: true, created: false, appended: 0, skipped: true, reason: 'not-a-git-repo' };
|
|
70
23
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
24
|
+
if (!existsSyncFn(gitignorePath)) {
|
|
25
|
+
const content = [...WORKFLOW_ENTRIES].join('\n') + '\n';
|
|
26
|
+
writeFileSyncFn(gitignorePath, content, 'utf8');
|
|
27
|
+
logFn(`[gitignore] Created ${gitignorePath} with ${WORKFLOW_ENTRIES.length} entries`);
|
|
28
|
+
return { ok: true, created: true, appended: WORKFLOW_ENTRIES.length, skipped: false };
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const stat = lstatSyncFn(gitignorePath);
|
|
32
|
+
if (stat.isSymbolicLink()) {
|
|
33
|
+
logFn(`[gitignore] Skipping ${gitignorePath}: symbolic link, not modifying`);
|
|
34
|
+
return { ok: true, created: false, appended: 0, skipped: true, reason: 'symlink' };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch (_err) {
|
|
38
|
+
// If we can't stat, fall through to read
|
|
39
|
+
}
|
|
40
|
+
let existingContent;
|
|
41
|
+
try {
|
|
42
|
+
existingContent = readFileSyncFn(gitignorePath, 'utf8');
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
46
|
+
logFn(`[gitignore] Could not read ${gitignorePath}: ${message}`);
|
|
47
|
+
return { ok: false, created: false, appended: 0, skipped: false, reason: `read-error: ${message}` };
|
|
48
|
+
}
|
|
49
|
+
const existingLines = existingContent.split(/\r?\n/);
|
|
50
|
+
const existingEntries = new Set(existingLines
|
|
51
|
+
.map((line) => line.trim())
|
|
52
|
+
.filter((line) => line.length > 0 && !line.startsWith('#')));
|
|
53
|
+
const missingEntries = WORKFLOW_ENTRIES.filter(entry => !existingEntries.has(entry));
|
|
54
|
+
if (missingEntries.length === 0) {
|
|
55
|
+
logFn(`[gitignore] ${gitignorePath} already contains all ${WORKFLOW_ENTRIES.length} workflow entries`);
|
|
56
|
+
return { ok: true, created: false, appended: 0, skipped: false };
|
|
57
|
+
}
|
|
58
|
+
const newContent = existingContent + '\n' + [...missingEntries].join('\n') + '\n';
|
|
59
|
+
writeFileSyncFn(gitignorePath, newContent, 'utf8');
|
|
60
|
+
logFn(`[gitignore] Appended ${missingEntries.length} missing entries to ${gitignorePath}`);
|
|
61
|
+
return { ok: true, created: false, appended: missingEntries.length, skipped: false };
|
|
108
62
|
}
|
|
109
|
-
|
|
63
|
+
ensureWorkflowGitignore.WORKFLOW_ENTRIES = WORKFLOW_ENTRIES;
|
|
64
|
+
ensureWorkflowGitignore.ensureWorkflowGitignore = ensureWorkflowGitignore;
|
|
110
65
|
module.exports = ensureWorkflowGitignore;
|
|
111
|
-
module.exports.ensureWorkflowGitignore = ensureWorkflowGitignore;
|
|
112
|
-
module.exports.WORKFLOW_ENTRIES = WORKFLOW_ENTRIES;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const WORKFLOW_ENTRIES = [
|
|
5
|
+
'.workflow/',
|
|
6
|
+
'.sessions/',
|
|
7
|
+
'.forgejo-local/',
|
|
8
|
+
'workflow/.cache/',
|
|
9
|
+
'workflow/.sessions/',
|
|
10
|
+
'workflow/config/agents.local.json',
|
|
11
|
+
'agents.local.json',
|
|
12
|
+
] as const;
|
|
13
|
+
|
|
14
|
+
interface GitignoreResult {
|
|
15
|
+
ok: boolean;
|
|
16
|
+
created: boolean;
|
|
17
|
+
appended: number;
|
|
18
|
+
skipped: boolean;
|
|
19
|
+
reason?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface EnsureOptions {
|
|
23
|
+
existsSyncFn?: typeof fs.existsSync;
|
|
24
|
+
lstatSyncFn?: typeof fs.lstatSync;
|
|
25
|
+
readFileSyncFn?: typeof fs.readFileSync;
|
|
26
|
+
writeFileSyncFn?: typeof fs.writeFileSync;
|
|
27
|
+
logFn?: (msg: string) => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type EnsureWorkflowGitignoreFn = {
|
|
31
|
+
(rootDir: string, options?: EnsureOptions): GitignoreResult;
|
|
32
|
+
WORKFLOW_ENTRIES: typeof WORKFLOW_ENTRIES;
|
|
33
|
+
ensureWorkflowGitignore: EnsureWorkflowGitignoreFn;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function ensureWorkflowGitignore(rootDir: string, options: EnsureOptions = {}): GitignoreResult {
|
|
37
|
+
const {
|
|
38
|
+
existsSyncFn = fs.existsSync,
|
|
39
|
+
lstatSyncFn = fs.lstatSync,
|
|
40
|
+
readFileSyncFn = fs.readFileSync,
|
|
41
|
+
writeFileSyncFn = fs.writeFileSync,
|
|
42
|
+
logFn = () => {},
|
|
43
|
+
} = options;
|
|
44
|
+
|
|
45
|
+
const gitignorePath = path.join(rootDir, '.gitignore');
|
|
46
|
+
const gitDirPath = path.join(rootDir, '.git');
|
|
47
|
+
|
|
48
|
+
if (!existsSyncFn(gitDirPath)) {
|
|
49
|
+
logFn(`[gitignore] Skipping ${rootDir}: not a git repository (no .git directory)`);
|
|
50
|
+
return { ok: true, created: false, appended: 0, skipped: true, reason: 'not-a-git-repo' };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!existsSyncFn(gitignorePath)) {
|
|
54
|
+
const content = [...WORKFLOW_ENTRIES].join('\n') + '\n';
|
|
55
|
+
writeFileSyncFn(gitignorePath, content, 'utf8');
|
|
56
|
+
logFn(`[gitignore] Created ${gitignorePath} with ${WORKFLOW_ENTRIES.length} entries`);
|
|
57
|
+
return { ok: true, created: true, appended: WORKFLOW_ENTRIES.length, skipped: false };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const stat = lstatSyncFn(gitignorePath);
|
|
62
|
+
if (stat.isSymbolicLink()) {
|
|
63
|
+
logFn(`[gitignore] Skipping ${gitignorePath}: symbolic link, not modifying`);
|
|
64
|
+
return { ok: true, created: false, appended: 0, skipped: true, reason: 'symlink' };
|
|
65
|
+
}
|
|
66
|
+
} catch (_err: unknown) {
|
|
67
|
+
// If we can't stat, fall through to read
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let existingContent: string;
|
|
71
|
+
try {
|
|
72
|
+
existingContent = readFileSyncFn(gitignorePath, 'utf8');
|
|
73
|
+
} catch (err: unknown) {
|
|
74
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
75
|
+
logFn(`[gitignore] Could not read ${gitignorePath}: ${message}`);
|
|
76
|
+
return { ok: false, created: false, appended: 0, skipped: false, reason: `read-error: ${message}` };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const existingLines = existingContent.split(/\r?\n/);
|
|
80
|
+
const existingEntries = new Set(
|
|
81
|
+
existingLines
|
|
82
|
+
.map((line: string) => line.trim())
|
|
83
|
+
.filter((line: string) => line.length > 0 && !line.startsWith('#'))
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const missingEntries = WORKFLOW_ENTRIES.filter(entry => !existingEntries.has(entry));
|
|
87
|
+
|
|
88
|
+
if (missingEntries.length === 0) {
|
|
89
|
+
logFn(`[gitignore] ${gitignorePath} already contains all ${WORKFLOW_ENTRIES.length} workflow entries`);
|
|
90
|
+
return { ok: true, created: false, appended: 0, skipped: false };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const newContent = existingContent + '\n' + [...missingEntries].join('\n') + '\n';
|
|
94
|
+
writeFileSyncFn(gitignorePath, newContent, 'utf8');
|
|
95
|
+
logFn(`[gitignore] Appended ${missingEntries.length} missing entries to ${gitignorePath}`);
|
|
96
|
+
|
|
97
|
+
return { ok: true, created: false, appended: missingEntries.length, skipped: false };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
(ensureWorkflowGitignore as EnsureWorkflowGitignoreFn).WORKFLOW_ENTRIES = WORKFLOW_ENTRIES;
|
|
101
|
+
(ensureWorkflowGitignore as EnsureWorkflowGitignoreFn).ensureWorkflowGitignore = ensureWorkflowGitignore as EnsureWorkflowGitignoreFn;
|
|
102
|
+
|
|
103
|
+
export = ensureWorkflowGitignore;
|