@glrs-dev/cli 2.4.0 → 2.4.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.
@@ -0,0 +1,249 @@
1
+ // src/spec-schema.ts
2
+ function validateMainSpec(raw) {
3
+ const errors = [];
4
+ if (typeof raw !== "object" || raw === null) {
5
+ return { valid: false, errors: ["main spec must be an object"] };
6
+ }
7
+ const obj = raw;
8
+ if (!("phases" in obj)) {
9
+ errors.push("main spec missing required field: phases");
10
+ } else if (!Array.isArray(obj["phases"])) {
11
+ errors.push("main spec field 'phases' must be an array");
12
+ } else {
13
+ const phases = obj["phases"];
14
+ for (let i = 0; i < phases.length; i++) {
15
+ const phase = phases[i];
16
+ if (typeof phase !== "object" || phase === null) {
17
+ errors.push(`phases[${i}] must be an object`);
18
+ continue;
19
+ }
20
+ const p = phase;
21
+ if (!("file" in p) || typeof p["file"] !== "string") {
22
+ errors.push(`phases[${i}] missing required field: file`);
23
+ }
24
+ if (!("completed" in p) || typeof p["completed"] !== "boolean") {
25
+ errors.push(`phases[${i}] missing required field: completed (boolean)`);
26
+ }
27
+ }
28
+ }
29
+ return { valid: errors.length === 0, errors };
30
+ }
31
+ function validatePhaseSpec(raw) {
32
+ const errors = [];
33
+ if (typeof raw !== "object" || raw === null) {
34
+ return { valid: false, errors: ["phase spec must be an object"] };
35
+ }
36
+ const obj = raw;
37
+ if (!("items" in obj)) {
38
+ errors.push("phase spec missing required field: items");
39
+ return { valid: false, errors };
40
+ }
41
+ if (!Array.isArray(obj["items"])) {
42
+ errors.push("phase spec field 'items' must be an array");
43
+ return { valid: false, errors };
44
+ }
45
+ const items = obj["items"];
46
+ for (let i = 0; i < items.length; i++) {
47
+ const item = items[i];
48
+ if (typeof item !== "object" || item === null) {
49
+ errors.push(`items[${i}] must be an object`);
50
+ continue;
51
+ }
52
+ const it = item;
53
+ if (!("id" in it) || typeof it["id"] !== "string" || !it["id"]) {
54
+ errors.push(`items[${i}] missing required field: id`);
55
+ }
56
+ if (!("intent" in it) || typeof it["intent"] !== "string" || !it["intent"]) {
57
+ errors.push(`items[${i}] missing required field: intent`);
58
+ }
59
+ }
60
+ return { valid: errors.length === 0, errors };
61
+ }
62
+
63
+ // src/spec-parser.ts
64
+ import * as fs from "fs";
65
+ import * as path from "path";
66
+ import { parse as yamlParse } from "yaml";
67
+ var DEGRADED = {
68
+ type: "single",
69
+ totalItems: 0,
70
+ checkedItems: 0,
71
+ phaseCount: 0,
72
+ phasesCompleted: 0,
73
+ phases: []
74
+ };
75
+ function hasSpec(planDir) {
76
+ try {
77
+ return fs.existsSync(path.join(planDir, "spec", "main.yaml"));
78
+ } catch {
79
+ return false;
80
+ }
81
+ }
82
+ function specItemToPlanItem(item) {
83
+ const files = (item.files ?? []).map((f) => ({
84
+ path: f.path,
85
+ isNew: f.isNew ?? false,
86
+ change: f.change ?? ""
87
+ }));
88
+ return {
89
+ id: item.id,
90
+ intent: item.intent,
91
+ checked: item.checked ?? false,
92
+ files,
93
+ tests: item.tests ?? [],
94
+ verify: item.verify ?? "",
95
+ ...item.mirror !== void 0 ? { mirror: item.mirror } : {},
96
+ ...item.context !== void 0 ? { context: item.context } : {},
97
+ ...item.conventions !== void 0 ? { conventions: item.conventions } : {}
98
+ };
99
+ }
100
+ function parseSpecItems(phasePath) {
101
+ try {
102
+ const content = fs.readFileSync(phasePath, "utf-8");
103
+ const raw = yamlParse(content);
104
+ const validation = validatePhaseSpec(raw);
105
+ if (!validation.valid) {
106
+ return [];
107
+ }
108
+ const spec = raw;
109
+ return spec.items.map(specItemToPlanItem);
110
+ } catch {
111
+ return [];
112
+ }
113
+ }
114
+ function parseSpecState(planDir) {
115
+ try {
116
+ const mainPath = path.join(planDir, "spec", "main.yaml");
117
+ if (!fs.existsSync(mainPath)) {
118
+ return { ...DEGRADED };
119
+ }
120
+ const mainContent = fs.readFileSync(mainPath, "utf-8");
121
+ const rawMain = yamlParse(mainContent);
122
+ const mainValidation = validateMainSpec(rawMain);
123
+ if (!mainValidation.valid) {
124
+ return { ...DEGRADED, type: "multi" };
125
+ }
126
+ const mainSpec = rawMain;
127
+ const phases = [];
128
+ let totalItems = 0;
129
+ let checkedItems = 0;
130
+ let phasesCompleted = 0;
131
+ for (const phaseRef of mainSpec.phases) {
132
+ const phasePath = path.join(planDir, "spec", phaseRef.file);
133
+ const items = parseSpecItems(phasePath);
134
+ const phaseTotal = items.length;
135
+ const phaseChecked = items.filter((it) => it.checked).length;
136
+ phases.push({
137
+ file: phaseRef.file,
138
+ totalItems: phaseTotal,
139
+ checkedItems: phaseChecked
140
+ });
141
+ totalItems += phaseTotal;
142
+ checkedItems += phaseChecked;
143
+ if (phaseRef.completed) {
144
+ phasesCompleted++;
145
+ }
146
+ }
147
+ return {
148
+ type: "multi",
149
+ totalItems,
150
+ checkedItems,
151
+ phaseCount: mainSpec.phases.length,
152
+ phasesCompleted,
153
+ phases
154
+ };
155
+ } catch {
156
+ return { ...DEGRADED };
157
+ }
158
+ }
159
+ function detectSpecPhases(planDir) {
160
+ try {
161
+ const mainPath = path.join(planDir, "spec", "main.yaml");
162
+ if (!fs.existsSync(mainPath)) {
163
+ return [];
164
+ }
165
+ const content = fs.readFileSync(mainPath, "utf-8");
166
+ const raw = yamlParse(content);
167
+ const validation = validateMainSpec(raw);
168
+ if (!validation.valid) {
169
+ return [];
170
+ }
171
+ const spec = raw;
172
+ return spec.phases.map((p) => p.file);
173
+ } catch {
174
+ return [];
175
+ }
176
+ }
177
+ function readSpecGoal(planDir) {
178
+ try {
179
+ const mainPath = path.join(planDir, "spec", "main.yaml");
180
+ const content = fs.readFileSync(mainPath, "utf-8");
181
+ const raw = yamlParse(content);
182
+ if (typeof raw === "object" && raw !== null) {
183
+ const obj = raw;
184
+ if (typeof obj["goal"] === "string") return obj["goal"];
185
+ }
186
+ return "";
187
+ } catch {
188
+ return "";
189
+ }
190
+ }
191
+ function readSpecTitle(planDir) {
192
+ try {
193
+ const mainPath = path.join(planDir, "spec", "main.yaml");
194
+ const content = fs.readFileSync(mainPath, "utf-8");
195
+ const raw = yamlParse(content);
196
+ if (typeof raw === "object" && raw !== null) {
197
+ const obj = raw;
198
+ if (typeof obj["title"] === "string") return obj["title"];
199
+ }
200
+ return "";
201
+ } catch {
202
+ return "";
203
+ }
204
+ }
205
+ function readSpecConstraints(planDir) {
206
+ try {
207
+ const mainPath = path.join(planDir, "spec", "main.yaml");
208
+ const content = fs.readFileSync(mainPath, "utf-8");
209
+ const raw = yamlParse(content);
210
+ if (typeof raw === "object" && raw !== null) {
211
+ const obj = raw;
212
+ if (typeof obj["constraints"] === "string") return obj["constraints"];
213
+ }
214
+ return "";
215
+ } catch {
216
+ return "";
217
+ }
218
+ }
219
+ function filterUncheckedSpecPhases(phaseFiles, planDir) {
220
+ try {
221
+ const mainPath = path.join(planDir, "spec", "main.yaml");
222
+ const content = fs.readFileSync(mainPath, "utf-8");
223
+ const raw = yamlParse(content);
224
+ const validation = validateMainSpec(raw);
225
+ if (!validation.valid) {
226
+ return phaseFiles;
227
+ }
228
+ const spec = raw;
229
+ const completedSet = new Set(
230
+ spec.phases.filter((p) => p.completed === true).map((p) => p.file)
231
+ );
232
+ return phaseFiles.filter((f) => !completedSet.has(f));
233
+ } catch {
234
+ return phaseFiles;
235
+ }
236
+ }
237
+
238
+ export {
239
+ validateMainSpec,
240
+ validatePhaseSpec,
241
+ hasSpec,
242
+ parseSpecItems,
243
+ parseSpecState,
244
+ detectSpecPhases,
245
+ readSpecGoal,
246
+ readSpecTitle,
247
+ readSpecConstraints,
248
+ filterUncheckedSpecPhases
249
+ };
@@ -0,0 +1,91 @@
1
+ import {
2
+ hasSpec,
3
+ readSpecGoal,
4
+ readSpecTitle
5
+ } from "./chunk-7OSEI5TF.js";
6
+
7
+ // src/changeset-generator.ts
8
+ import * as fs from "fs";
9
+ import * as path from "path";
10
+ var TARGET_PACKAGE = "@glrs-dev/harness-plugin-opencode";
11
+ function readPlanTitle(planPath) {
12
+ try {
13
+ const stat = fs.statSync(planPath);
14
+ if (stat.isDirectory() && hasSpec(planPath)) {
15
+ const yamlTitle = readSpecTitle(planPath);
16
+ if (yamlTitle) return yamlTitle;
17
+ }
18
+ const target = stat.isDirectory() ? path.join(planPath, "main.md") : planPath;
19
+ const content = fs.readFileSync(target, "utf-8");
20
+ const match = content.match(/^#\s+(.+?)\s*$/m);
21
+ return match ? match[1].trim() : "";
22
+ } catch {
23
+ return "";
24
+ }
25
+ }
26
+ function readPlanGoal(planPath) {
27
+ try {
28
+ const stat = fs.statSync(planPath);
29
+ if (stat.isDirectory() && hasSpec(planPath)) {
30
+ const yamlGoal = readSpecGoal(planPath);
31
+ if (yamlGoal) return yamlGoal;
32
+ }
33
+ const target = stat.isDirectory() ? path.join(planPath, "main.md") : planPath;
34
+ const content = fs.readFileSync(target, "utf-8");
35
+ const re = /^##\s+Goal\s*\n([\s\S]*?)(?=^##\s|$)/m;
36
+ const match = content.match(re);
37
+ if (match) {
38
+ return match[1].trim().replace(/\s+/g, " ");
39
+ }
40
+ return readPlanTitle(planPath);
41
+ } catch {
42
+ return "";
43
+ }
44
+ }
45
+ function inferBumpLevel(title) {
46
+ const t = title.toLowerCase();
47
+ if (t.includes("remove ") || t.includes("removal") || t.includes("breaking") || t.includes("break ") || /\bv2\b/.test(t) || /\bv\d+\b/.test(t.replace(/v1\b/, ""))) {
48
+ return "major";
49
+ }
50
+ if (/\bfix\b/.test(t) || /\bbug(s|fix)?\b/.test(t) || t.includes("hotfix")) {
51
+ return "patch";
52
+ }
53
+ return "minor";
54
+ }
55
+ function slugifyTitle(title) {
56
+ const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
57
+ return slug.length > 0 ? slug : "autopilot";
58
+ }
59
+ function defaultRandomSuffix() {
60
+ return Math.random().toString(36).slice(2, 8);
61
+ }
62
+ async function generateChangeset(planPath, repoRoot, opts = {}) {
63
+ const packageName = opts.packageName ?? TARGET_PACKAGE;
64
+ const randomSuffix = opts._randomSuffix ?? defaultRandomSuffix;
65
+ const title = readPlanTitle(planPath) || "Autopilot run";
66
+ const goal = readPlanGoal(planPath) || title;
67
+ const bumpLevel = inferBumpLevel(title);
68
+ const slug = slugifyTitle(title);
69
+ const content = `---
70
+ "${packageName}": ${bumpLevel}
71
+ ---
72
+
73
+ ${goal}
74
+ `;
75
+ const changesetDir = path.join(repoRoot, ".changeset");
76
+ if (!fs.existsSync(changesetDir)) {
77
+ fs.mkdirSync(changesetDir, { recursive: true });
78
+ }
79
+ const filename = `${slug}-${randomSuffix()}.md`;
80
+ const filePath = path.join(changesetDir, filename);
81
+ fs.writeFileSync(filePath, content, "utf-8");
82
+ return { path: filePath, content, bumpLevel };
83
+ }
84
+
85
+ export {
86
+ readPlanTitle,
87
+ readPlanGoal,
88
+ inferBumpLevel,
89
+ slugifyTitle,
90
+ generateChangeset
91
+ };
@@ -0,0 +1,101 @@
1
+ import {
2
+ hasSpec,
3
+ readSpecTitle
4
+ } from "./chunk-7OSEI5TF.js";
5
+
6
+ // src/auto-ship.ts
7
+ import { execFile as execFileCb } from "child_process";
8
+ import { promisify } from "util";
9
+ import * as fs from "fs";
10
+ import * as path from "path";
11
+ var execFileDefault = promisify(execFileCb);
12
+ var FORBIDDEN_BRANCHES = /* @__PURE__ */ new Set(["main", "master"]);
13
+ function resolvePlanMainPath(planPath) {
14
+ try {
15
+ if (fs.statSync(planPath).isDirectory()) {
16
+ return path.join(planPath, "main.md");
17
+ }
18
+ } catch {
19
+ }
20
+ return planPath;
21
+ }
22
+ function readPlanH1(planPath) {
23
+ try {
24
+ if (fs.statSync(planPath).isDirectory() && hasSpec(planPath)) {
25
+ const yamlTitle = readSpecTitle(planPath);
26
+ if (yamlTitle) return yamlTitle;
27
+ }
28
+ } catch {
29
+ }
30
+ const target = resolvePlanMainPath(planPath);
31
+ try {
32
+ const content = fs.readFileSync(target, "utf-8");
33
+ const match = content.match(/^#\s+(.+?)\s*$/m);
34
+ if (match) return match[1].trim();
35
+ } catch {
36
+ }
37
+ return "Autopilot run";
38
+ }
39
+ async function autoShip(opts) {
40
+ const execFile = opts._deps?.execFile ?? execFileDefault;
41
+ const cwd = opts.repoRoot;
42
+ let branch;
43
+ try {
44
+ const { stdout } = await execFile(
45
+ "git",
46
+ ["rev-parse", "--abbrev-ref", "HEAD"],
47
+ { cwd }
48
+ );
49
+ branch = (typeof stdout === "string" ? stdout : String(stdout)).trim();
50
+ } catch (err) {
51
+ const msg = err instanceof Error ? err.message : String(err);
52
+ throw new Error(`auto-ship: failed to resolve current branch: ${msg}`);
53
+ }
54
+ if (!branch || branch === "HEAD") {
55
+ throw new Error(
56
+ `auto-ship: refusing to ship from a detached HEAD (branch="${branch}")`
57
+ );
58
+ }
59
+ if (FORBIDDEN_BRANCHES.has(branch)) {
60
+ throw new Error(
61
+ `auto-ship: refusing to push to forbidden branch "${branch}". Create a feature branch first.`
62
+ );
63
+ }
64
+ try {
65
+ await execFile("git", ["push", "-u", "origin", branch], { cwd });
66
+ } catch (err) {
67
+ const msg = err instanceof Error ? err.message : String(err);
68
+ throw new Error(`auto-ship: git push failed: ${msg}`);
69
+ }
70
+ const title = readPlanH1(opts.planPath);
71
+ const bodyFile = resolvePlanMainPath(opts.planPath);
72
+ if (!fs.existsSync(bodyFile)) {
73
+ throw new Error(`auto-ship: plan body file not found: ${bodyFile}`);
74
+ }
75
+ let prUrl;
76
+ try {
77
+ const { stdout } = await execFile(
78
+ "gh",
79
+ [
80
+ "pr",
81
+ "create",
82
+ "--title",
83
+ title,
84
+ "--body-file",
85
+ bodyFile
86
+ ],
87
+ { cwd }
88
+ );
89
+ const text = typeof stdout === "string" ? stdout : String(stdout);
90
+ const urlMatch = text.match(/https?:\/\/\S+/);
91
+ prUrl = urlMatch ? urlMatch[0] : text.trim();
92
+ } catch (err) {
93
+ const msg = err instanceof Error ? err.message : String(err);
94
+ throw new Error(`auto-ship: gh pr create failed: ${msg}`);
95
+ }
96
+ return { prUrl, branch, title };
97
+ }
98
+
99
+ export {
100
+ autoShip
101
+ };
@@ -0,0 +1,68 @@
1
+ // src/lib/logger.ts
2
+ import pino from "pino";
3
+ import { existsSync, mkdirSync } from "fs";
4
+ import { dirname, join } from "path";
5
+ function resolveLogFilePath(cwd) {
6
+ const env = process.env["GLRS_AUTOPILOT_LOG_FILE"];
7
+ if (env === "off") return null;
8
+ if (env) return env;
9
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
10
+ return join(cwd, ".agent", "autopilot-logs", `${timestamp}.log`);
11
+ }
12
+ function buildFileStream(cwd) {
13
+ const filePath = resolveLogFilePath(cwd);
14
+ if (!filePath) return null;
15
+ const parent = dirname(filePath);
16
+ if (!existsSync(parent)) {
17
+ mkdirSync(parent, { recursive: true });
18
+ }
19
+ return {
20
+ path: filePath,
21
+ entry: {
22
+ level: "trace",
23
+ // sync: true gives deterministic flushSync semantics so the file
24
+ // log is safe to read immediately after flush() returns (critical
25
+ // for tests and for reliable postmortem after a crash). Autopilot
26
+ // runs are long-lived enough that sync writes aren't a bottleneck.
27
+ stream: pino.destination({ dest: filePath, sync: true, mkdir: true })
28
+ }
29
+ };
30
+ }
31
+ function createAutopilotLogger(opts) {
32
+ const fileSink = buildFileStream(opts.cwd);
33
+ const streams = [];
34
+ if (fileSink) streams.push(fileSink.entry);
35
+ if (streams.length === 0) {
36
+ const root2 = pino({ level: "silent" });
37
+ return { root: root2, logFilePath: null, flush: async () => {
38
+ } };
39
+ }
40
+ const ms = pino.multistream(streams);
41
+ const root = pino(
42
+ {
43
+ level: "trace",
44
+ // file sink captures everything
45
+ timestamp: pino.stdTimeFunctions.isoTime
46
+ },
47
+ ms
48
+ );
49
+ const flush = async () => {
50
+ try {
51
+ ms.flushSync();
52
+ } catch {
53
+ }
54
+ };
55
+ return {
56
+ root,
57
+ logFilePath: fileSink?.path ?? null,
58
+ flush
59
+ };
60
+ }
61
+ function childLogger(root, component) {
62
+ return root.child({ component });
63
+ }
64
+
65
+ export {
66
+ createAutopilotLogger,
67
+ childLogger
68
+ };