@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.
Files changed (51) hide show
  1. package/lib/agents/agents.js +737 -838
  2. package/lib/agents/agents.ts +930 -0
  3. package/lib/agents/claude-telemetry.js +149 -154
  4. package/lib/agents/claude-telemetry.ts +233 -0
  5. package/lib/agents/claude.js +121 -131
  6. package/lib/agents/claude.ts +156 -0
  7. package/lib/agents/codex-telemetry.js +176 -164
  8. package/lib/agents/codex-telemetry.ts +205 -0
  9. package/lib/agents/codex.js +168 -201
  10. package/lib/agents/codex.ts +236 -0
  11. package/lib/agents/limit-hit.js +216 -226
  12. package/lib/agents/limit-hit.ts +262 -0
  13. package/lib/agents/mistral-telemetry.js +7 -10
  14. package/lib/agents/mistral-telemetry.ts +44 -0
  15. package/lib/agents/mistral.js +40 -58
  16. package/lib/agents/mistral.ts +84 -0
  17. package/lib/agents/opencode-export.js +126 -127
  18. package/lib/agents/opencode-export.ts +160 -0
  19. package/lib/agents/opencode-telemetry.js +282 -261
  20. package/lib/agents/opencode-telemetry.ts +350 -0
  21. package/lib/agents/opencode.js +263 -305
  22. package/lib/agents/opencode.ts +370 -0
  23. package/lib/agents/stage-telemetry.js +23 -24
  24. package/lib/agents/stage-telemetry.ts +47 -0
  25. package/lib/commands/integrate.js +9 -3
  26. package/lib/core/fmt.js +140 -211
  27. package/lib/core/fmt.ts +189 -0
  28. package/lib/core/git.js +121 -102
  29. package/lib/core/git.ts +146 -0
  30. package/lib/core/gitignore.js +59 -106
  31. package/lib/core/gitignore.ts +103 -0
  32. package/lib/core/mission-utils.js +833 -875
  33. package/lib/core/mission-utils.ts +998 -0
  34. package/lib/core/persistent-data-migration.js +247 -205
  35. package/lib/core/persistent-data-migration.ts +255 -0
  36. package/lib/core/product-config.js +400 -476
  37. package/lib/core/product-config.ts +518 -0
  38. package/lib/core/runtime-matrix.js +41 -87
  39. package/lib/core/runtime-matrix.ts +84 -0
  40. package/lib/core/spawn-tee.js +152 -192
  41. package/lib/core/spawn-tee.ts +202 -0
  42. package/lib/core/state-map.js +92 -92
  43. package/lib/core/state-map.ts +115 -0
  44. package/lib/core/storage.js +101 -129
  45. package/lib/core/storage.ts +182 -0
  46. package/lib/core/verification.js +148 -138
  47. package/lib/core/verification.ts +200 -0
  48. package/package.json +4 -2
  49. package/prompts/draft.md +1 -0
  50. package/prompts/portfolio.md +1 -1
  51. package/px.js +0 -2
package/lib/core/git.js CHANGED
@@ -1,121 +1,140 @@
1
- const childProcess = require('child_process');
2
-
3
- /** @param {string[]} args @param {object} options */
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
- const spawnOptions = {
6
- encoding: 'utf8',
7
- stdio: ['ignore', 'pipe', 'pipe'],
8
- ...options
9
- };
10
- const result = childProcess.spawnSync('git', args, /** @type {import('child_process').SpawnSyncOptionsWithStringEncoding} */(spawnOptions));
11
- if (result.error && result.status === null) {
12
- throw result.error;
13
- }
14
- return result;
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
- const spawnOptions = {
20
- encoding: 'utf8',
21
- stdio: ['ignore', 'pipe', 'pipe'],
22
- ...options
23
- };
24
- const result = childProcess.spawnSync(command, args, /** @type {import('child_process').SpawnSyncOptionsWithStringEncoding} */(spawnOptions));
25
- if (result.error && result.status === null) {
26
- throw result.error;
27
- }
28
- return result;
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
- const result = git(['-C', cwd, 'branch', '--show-current']);
33
- return result.stdout.trim();
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
- const result = git(['-C', cwd, 'status', '--porcelain']);
38
- return result.stdout
39
- .split('\n')
40
- .map(line => line.trimEnd())
41
- .filter(Boolean);
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
- const result = git(['-C', cwd, 'status', '--porcelain']);
46
- return result.stdout.trim().length > 0;
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
- const result = git(['-C', cwd, 'status', '--porcelain']);
51
- if (!result.stdout.trim()) {return 0;}
52
- return result.stdout.trim().split('\n').length;
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
- return Array.from(new Set(
57
- output
58
- .split('\n')
59
- .map(line => line.trim())
60
- .filter(Boolean)
61
- .map(line => line.split('\t')[1])
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
- function detectRebaseState(cwd = process.cwd(), { gitRunner = git, fsModule = require('fs'), pathModule = require('path') } = {}) {
67
- const gitDirResult = gitRunner(['-C', cwd, 'rev-parse', '--git-dir']);
68
- const resolvedGitDir = gitDirResult.status === 0
69
- ? gitDirResult.stdout.trim()
70
- : '.git';
71
- const gitDir = pathModule.isAbsolute(resolvedGitDir)
72
- ? resolvedGitDir
73
- : pathModule.join(cwd, resolvedGitDir);
74
- const rebaseMergeDir = pathModule.join(gitDir, 'rebase-merge');
75
- const rebaseApplyDir = pathModule.join(gitDir, 'rebase-apply');
76
- const rebaseDir = fsModule.existsSync(rebaseMergeDir)
77
- ? rebaseMergeDir
78
- : (fsModule.existsSync(rebaseApplyDir) ? rebaseApplyDir : null);
79
-
80
- const headResult = gitRunner(['-C', cwd, 'symbolic-ref', '--quiet', '--short', 'HEAD']);
81
- const detached = headResult.status !== 0;
82
-
83
- const showCurrentResult = gitRunner(['-C', cwd, 'rebase', '--show-current']);
84
- const rebaseHead = showCurrentResult.status === 0 ? showCurrentResult.stdout.trim() : '';
85
-
86
- const unmergedResult = gitRunner(['-C', cwd, 'ls-files', '-u']);
87
- const unmergedFiles = unmergedResult.status === 0 ? parseUnmergedFiles(unmergedResult.stdout) : [];
88
-
89
- const inProgress = Boolean(rebaseDir || rebaseHead || unmergedFiles.length > 0);
90
-
91
- return {
92
- inProgress,
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
- const result = git(['log', '-1', '--format=%H|%ad|%s']);
102
- const [sha, date, subject] = result.stdout.trim().split('|');
103
- return { sha, date, subject };
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
- const result = git(['log', '-3', '--format=%s']);
108
- return result.stdout.trim().split('\n');
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
- };
@@ -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
+ }
@@ -1,112 +1,65 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
-
4
- /**
5
- * Workflow-generated paths that must be gitignored in enterprise repositories.
6
- * These are paths created by the px workflow toolkit that should never be
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
- '.workflow/',
11
- '.sessions/',
12
- '.forgejo-local/',
13
- 'workflow/.cache/',
14
- 'workflow/.sessions/',
15
- 'workflow/config/agents.local.json',
16
- 'agents.local.json',
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
- const {
39
- existsSyncFn = fs.existsSync,
40
- lstatSyncFn = fs.lstatSync,
41
- readFileSyncFn = fs.readFileSync,
42
- writeFileSyncFn = fs.writeFileSync,
43
- logFn = () => {},
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
- } catch (_) {
72
- // If we can't stat, fall through to read - it might exist but be inaccessible
73
- }
74
-
75
- // Read existing .gitignore
76
- let existingContent;
77
- try {
78
- existingContent = readFileSyncFn(gitignorePath, 'utf8');
79
- } catch (err) {
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
- }
83
-
84
- // Parse existing entries
85
- const existingLines = existingContent.split(/\r?\n/);
86
- const existingEntries = new Set(
87
- existingLines
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('#'))
92
- );
93
-
94
- // Find missing entries
95
- const missingEntries = WORKFLOW_ENTRIES.filter(entry => !existingEntries.has(entry));
96
-
97
- if (missingEntries.length === 0) {
98
- logFn(`[gitignore] ${gitignorePath} already contains all ${WORKFLOW_ENTRIES.length} workflow entries`);
99
- return { ok: true, created: false, appended: 0, skipped: false };
100
- }
101
-
102
- // Append missing entries
103
- const newContent = existingContent + '\n' + missingEntries.join('\n') + '\n';
104
- writeFileSyncFn(gitignorePath, newContent, 'utf8');
105
- logFn(`[gitignore] Appended ${missingEntries.length} missing entries to ${gitignorePath}`);
106
-
107
- return { ok: true, created: false, appended: missingEntries.length, skipped: false };
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;