@getpipher/armory-fleet 0.4.0 → 0.5.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.
@@ -0,0 +1,103 @@
1
+ 'use strict';
2
+
3
+ var CronExpression = require('./expression');
4
+
5
+ function CronParser() {}
6
+
7
+ /**
8
+ * Parse crontab entry
9
+ *
10
+ * @private
11
+ * @param {String} entry Crontab file entry/line
12
+ */
13
+ CronParser._parseEntry = function _parseEntry (entry) {
14
+ var atoms = entry.split(' ');
15
+
16
+ if (atoms.length === 6) {
17
+ return {
18
+ interval: CronExpression.parse(entry)
19
+ };
20
+ } else if (atoms.length > 6) {
21
+ return {
22
+ interval: CronExpression.parse(entry),
23
+ command: atoms.slice(6, atoms.length)
24
+ };
25
+ } else {
26
+ throw new Error('Invalid entry: ' + entry);
27
+ }
28
+ };
29
+
30
+ /**
31
+ * Wrapper for CronExpression.parser method
32
+ *
33
+ * @public
34
+ * @param {String} expression Input expression
35
+ * @param {Object} [options] Parsing options
36
+ * @return {Object}
37
+ */
38
+ CronParser.parseExpression = function parseExpression (expression, options, callback) {
39
+ return CronExpression.parse(expression, options, callback);
40
+ };
41
+
42
+ /**
43
+ * Parse content string
44
+ *
45
+ * @public
46
+ * @param {String} data Crontab content
47
+ * @return {Object}
48
+ */
49
+ CronParser.parseString = function parseString (data) {
50
+ var self = this;
51
+ var blocks = data.split('\n');
52
+
53
+ var response = {
54
+ variables: {},
55
+ expressions: [],
56
+ errors: {}
57
+ };
58
+
59
+ for (var i = 0, c = blocks.length; i < c; i++) {
60
+ var block = blocks[i];
61
+ var matches = null;
62
+ var entry = block.replace(/^\s+|\s+$/g, ''); // Remove surrounding spaces
63
+
64
+ if (entry.length > 0) {
65
+ if (entry.match(/^#/)) { // Comment
66
+ continue;
67
+ } else if ((matches = entry.match(/^(.*)=(.*)$/))) { // Variable
68
+ response.variables[matches[1]] = matches[2];
69
+ } else { // Expression?
70
+ var result = null;
71
+
72
+ try {
73
+ result = self._parseEntry('0 ' + entry);
74
+ response.expressions.push(result.interval);
75
+ } catch (err) {
76
+ response.errors[entry] = err;
77
+ }
78
+ }
79
+ }
80
+ }
81
+
82
+ return response;
83
+ };
84
+
85
+ /**
86
+ * Parse crontab file
87
+ *
88
+ * @public
89
+ * @param {String} filePath Path to file
90
+ * @param {Function} callback
91
+ */
92
+ CronParser.parseFile = function parseFile (filePath, callback) {
93
+ require('fs').readFile(filePath, function(err, data) {
94
+ if (err) {
95
+ callback(err);
96
+ return;
97
+ }
98
+
99
+ return callback(null, CronParser.parseString(data.toString()));
100
+ });
101
+ };
102
+
103
+ module.exports = CronParser;
@@ -0,0 +1,12 @@
1
+ // Type declarations for the vendored cron-parser lib (v1.1.1, CJS, dep-free).
2
+ declare module "../vendor/cron-parser/lib/parser.js" {
3
+ export interface CronExpressionIter {
4
+ next(): Date;
5
+ prev(): Date;
6
+ hasNext(): boolean;
7
+ }
8
+ export interface ParseOptions { currentDate?: Date; endDate?: Date; iterator?: boolean; }
9
+ export function parseExpression(expression: string, options?: ParseOptions): CronExpressionIter;
10
+ export function parseString(entry: string): unknown;
11
+ export function parseFile(filePath: string): unknown;
12
+ }
@@ -0,0 +1,40 @@
1
+ // src/worktree/diff-service.ts
2
+ // SPEC-5a §7 — worktree-diff artifact discovery for isolated runs (Q3=A).
3
+ // All changes in the worktree vs base: tracked modifications + untracked new files.
4
+ import { execSync } from "node:child_process";
5
+
6
+ export interface PhaseArtifacts {
7
+ paths: string[];
8
+ summary: string;
9
+ }
10
+
11
+ function sh(cmd: string, cwd: string): string {
12
+ return execSync(cmd, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).toString();
13
+ }
14
+
15
+ const MAX_SUMMARY = 200;
16
+
17
+ export class DiffService {
18
+ /**
19
+ * Compute a phase's artifacts = all changes in the worktree vs baseRef.
20
+ * Tracked modifications via `git diff --name-only`; untracked new files via
21
+ * `git status --porcelain` (?? entries). Deduped + sorted.
22
+ *
23
+ * @param childFinalText the child's final text, truncated to MAX_SUMMARY chars as the prose summary.
24
+ */
25
+ diffPhase(worktreePath: string, baseRef: string, childFinalText = ""): PhaseArtifacts {
26
+ const tracked = sh(`git diff --name-only ${baseRef} --`, worktreePath)
27
+ .split("\n")
28
+ .filter(Boolean);
29
+ const status = sh("git status --porcelain", worktreePath);
30
+ const untracked = status
31
+ .split("\n")
32
+ .filter((l) => l.startsWith("?? "))
33
+ .map((l) => l.slice(3).trim());
34
+ const paths = Array.from(new Set([...tracked, ...untracked])).sort();
35
+ const summary = childFinalText.length > MAX_SUMMARY
36
+ ? childFinalText.slice(0, MAX_SUMMARY - 1) + "…"
37
+ : childFinalText;
38
+ return { paths, summary };
39
+ }
40
+ }
@@ -0,0 +1,92 @@
1
+ // src/worktree/worktree-service.ts
2
+ // Greenfield git worktree lifecycle (SPEC-5a §6, Q9=A — thin shell-outs, no git library).
3
+ import { execSync } from "node:child_process";
4
+ import { existsSync, mkdirSync, rmSync } from "node:fs";
5
+ import { join } from "node:path";
6
+
7
+ export interface WorktreeRef {
8
+ path: string;
9
+ branch: string;
10
+ }
11
+
12
+ export interface WorktreeServiceOpts {
13
+ rootDir: string;
14
+ /** Where worktrees live. Defaults to <rootDir>/.pi/fleet/worktrees. */
15
+ worktreesDir?: string;
16
+ }
17
+
18
+ function sh(cmd: string, cwd: string): string {
19
+ return execSync(cmd, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).toString().trim();
20
+ }
21
+
22
+ export class WorktreeService {
23
+ private readonly rootDir: string;
24
+ private readonly worktreesDir: string;
25
+
26
+ constructor(opts: WorktreeServiceOpts) {
27
+ this.rootDir = opts.rootDir;
28
+ this.worktreesDir = opts.worktreesDir ?? join(opts.rootDir, ".pi", "fleet", "worktrees");
29
+ }
30
+
31
+ branchFor(runId: string): string {
32
+ return `fleet/${runId}`;
33
+ }
34
+
35
+ pathFor(runId: string): string {
36
+ return join(this.worktreesDir, runId);
37
+ }
38
+
39
+ exists(runId: string): boolean {
40
+ return existsSync(this.pathFor(runId));
41
+ }
42
+
43
+ create(runId: string, baseRef = "HEAD"): WorktreeRef {
44
+ if (this.exists(runId)) {
45
+ throw new Error(`worktree for run ${runId} already exists at ${this.pathFor(runId)}`);
46
+ }
47
+ mkdirSync(this.worktreesDir, { recursive: true });
48
+ const branch = this.branchFor(runId);
49
+ const path = this.pathFor(runId);
50
+ try {
51
+ sh(`git worktree add -b ${branch} ${path} ${baseRef}`, this.rootDir);
52
+ } catch (e) {
53
+ if (existsSync(path)) rmSync(path, { recursive: true, force: true });
54
+ const msg = (e as Error).message;
55
+ const tail = msg.split("\n").filter(Boolean).pop() ?? msg;
56
+ throw new Error(`worktree create failed for run ${runId} (base ${baseRef}): ${tail}`);
57
+ }
58
+ return { path, branch };
59
+ }
60
+
61
+ /** SPEC-5a: remove the worktree dir but KEEP the branch (for completed runs the branch is
62
+ * kept for merge/inspection; only the worktree dir is temporary scaffolding). */
63
+ removeWorktree(runId: string): void {
64
+ const path = this.pathFor(runId);
65
+ if (existsSync(path)) {
66
+ try {
67
+ sh(`git worktree remove --force ${path}`, this.rootDir);
68
+ } catch {
69
+ rmSync(path, { recursive: true, force: true });
70
+ try { sh("git worktree prune", this.rootDir); } catch { /* ignore */ }
71
+ }
72
+ }
73
+ }
74
+
75
+ remove(runId: string): void {
76
+ const path = this.pathFor(runId);
77
+ const branch = this.branchFor(runId);
78
+ if (existsSync(path)) {
79
+ try {
80
+ sh(`git worktree remove --force ${path}`, this.rootDir);
81
+ } catch {
82
+ rmSync(path, { recursive: true, force: true });
83
+ try { sh("git worktree prune", this.rootDir); } catch { /* ignore */ }
84
+ }
85
+ }
86
+ try {
87
+ sh(`git branch -D ${branch}`, this.rootDir);
88
+ } catch {
89
+ // branch may not exist; ignore
90
+ }
91
+ }
92
+ }