helloloop 0.1.0 → 0.1.2

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,211 @@
1
+ import { spawnSync } from "node:child_process";
2
+
3
+ function createUnavailableInvocation(message) {
4
+ return {
5
+ command: "",
6
+ argsPrefix: [],
7
+ shell: false,
8
+ error: message,
9
+ };
10
+ }
11
+
12
+ function parseWindowsCommandMatches(output) {
13
+ return String(output || "")
14
+ .split(/\r?\n/)
15
+ .map((line) => line.trim())
16
+ .filter(Boolean);
17
+ }
18
+
19
+ function hasCommand(command, platform = process.platform) {
20
+ if (platform === "win32") {
21
+ const result = spawnSync("where.exe", [command], {
22
+ encoding: "utf8",
23
+ shell: false,
24
+ });
25
+ return result.status === 0;
26
+ }
27
+
28
+ const result = spawnSync("sh", ["-lc", `command -v ${command}`], {
29
+ encoding: "utf8",
30
+ shell: false,
31
+ });
32
+ return result.status === 0;
33
+ }
34
+
35
+ function findWindowsCommandPaths(command, resolver) {
36
+ const lookup = resolver || ((name) => {
37
+ const result = spawnSync("where.exe", [name], {
38
+ encoding: "utf8",
39
+ shell: false,
40
+ });
41
+ return result.status === 0 ? parseWindowsCommandMatches(result.stdout) : [];
42
+ });
43
+
44
+ return lookup(command);
45
+ }
46
+
47
+ function resolveWindowsPowerShellHost(options = {}) {
48
+ const commandExists = options.commandExists || ((command) => hasCommand(command, "win32"));
49
+
50
+ if (commandExists("pwsh")) {
51
+ return {
52
+ command: "pwsh",
53
+ shell: false,
54
+ };
55
+ }
56
+
57
+ if (commandExists("powershell")) {
58
+ return {
59
+ command: "powershell",
60
+ shell: false,
61
+ };
62
+ }
63
+
64
+ return null;
65
+ }
66
+
67
+ function resolveWindowsCommandShell(options = {}) {
68
+ const commandExists = options.commandExists || ((command) => hasCommand(command, "win32"));
69
+ const preferredHosts = [
70
+ {
71
+ check: "pwsh",
72
+ invocation: {
73
+ command: "pwsh",
74
+ argsPrefix: ["-NoLogo", "-NoProfile", "-Command"],
75
+ shell: false,
76
+ },
77
+ },
78
+ {
79
+ check: "bash",
80
+ invocation: {
81
+ command: "bash",
82
+ argsPrefix: ["-lc"],
83
+ shell: false,
84
+ },
85
+ },
86
+ {
87
+ check: "powershell",
88
+ invocation: {
89
+ command: "powershell",
90
+ argsPrefix: ["-NoLogo", "-NoProfile", "-Command"],
91
+ shell: false,
92
+ },
93
+ },
94
+ ];
95
+
96
+ for (const host of preferredHosts) {
97
+ if (commandExists(host.check)) {
98
+ return host.invocation;
99
+ }
100
+ }
101
+
102
+ return null;
103
+ }
104
+
105
+ function resolvePosixCommandShell(options = {}) {
106
+ const platform = options.platform || process.platform;
107
+ const commandExists = options.commandExists || ((command) => hasCommand(command, platform));
108
+
109
+ if (commandExists("bash")) {
110
+ return {
111
+ command: "bash",
112
+ argsPrefix: ["-lc"],
113
+ shell: false,
114
+ };
115
+ }
116
+
117
+ return {
118
+ command: "sh",
119
+ argsPrefix: ["-lc"],
120
+ shell: false,
121
+ };
122
+ }
123
+
124
+ function isCmdLikeExecutable(executable) {
125
+ return /\.(cmd|bat)$/i.test(String(executable || ""));
126
+ }
127
+
128
+ function resolveWindowsCodexExecutable(options = {}) {
129
+ const explicitExecutable = String(options.explicitExecutable || "").trim();
130
+ const findCommandPaths = options.findCommandPaths || ((command) => findWindowsCommandPaths(command));
131
+
132
+ if (explicitExecutable) {
133
+ return explicitExecutable;
134
+ }
135
+
136
+ const searchOrder = ["codex.ps1", "codex.exe", "codex"];
137
+ for (const query of searchOrder) {
138
+ const safeMatch = findCommandPaths(query).find((candidate) => !isCmdLikeExecutable(candidate));
139
+ if (safeMatch) {
140
+ return safeMatch;
141
+ }
142
+ }
143
+
144
+ return "";
145
+ }
146
+
147
+ export function resolveVerifyShellInvocation(options = {}) {
148
+ const platform = options.platform || process.platform;
149
+
150
+ if (platform === "win32") {
151
+ const host = resolveWindowsCommandShell(options);
152
+ if (!host) {
153
+ return createUnavailableInvocation(
154
+ "Windows 环境需要 pwsh、bash(如 Git Bash)或 powershell 才能安全执行验证命令;HelloLoop 已禁止回退到 cmd.exe。",
155
+ );
156
+ }
157
+ return host;
158
+ }
159
+
160
+ return resolvePosixCommandShell(options);
161
+ }
162
+
163
+ export function resolveCodexInvocation(options = {}) {
164
+ const platform = options.platform || process.platform;
165
+ const explicitExecutable = String(options.explicitExecutable || "").trim();
166
+
167
+ if (platform !== "win32") {
168
+ return {
169
+ command: explicitExecutable || "codex",
170
+ argsPrefix: [],
171
+ shell: false,
172
+ };
173
+ }
174
+
175
+ const executable = resolveWindowsCodexExecutable({
176
+ explicitExecutable,
177
+ findCommandPaths: options.findCommandPaths,
178
+ });
179
+ if (!executable) {
180
+ return createUnavailableInvocation(
181
+ "未找到可安全执行的 codex 入口。Windows 环境需要 codex.ps1、codex.exe 或其他非 cmd 的可执行入口。",
182
+ );
183
+ }
184
+
185
+ if (isCmdLikeExecutable(executable)) {
186
+ return createUnavailableInvocation(
187
+ `HelloLoop 在 Windows 已禁止通过 cmd/bat 启动 Codex:${executable}`,
188
+ );
189
+ }
190
+
191
+ if (/\.ps1$/i.test(executable)) {
192
+ const host = resolveWindowsPowerShellHost(options);
193
+ if (!host) {
194
+ return createUnavailableInvocation(
195
+ `需要 pwsh 或 powershell 才能安全执行 ${executable};HelloLoop 不会回退到 cmd.exe。`,
196
+ );
197
+ }
198
+
199
+ return {
200
+ command: host.command,
201
+ argsPrefix: ["-NoLogo", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", executable],
202
+ shell: host.shell,
203
+ };
204
+ }
205
+
206
+ return {
207
+ command: executable,
208
+ argsPrefix: [],
209
+ shell: false,
210
+ };
211
+ }
@@ -9,4 +9,4 @@
9
9
  - 当前任务:无
10
10
  - 最近结果:HelloLoop 已初始化
11
11
  - 下一建议:先完善 backlog.json
12
-
12
+
@@ -0,0 +1,144 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "type": "object",
4
+ "additionalProperties": false,
5
+ "required": [
6
+ "project",
7
+ "summary",
8
+ "tasks"
9
+ ],
10
+ "properties": {
11
+ "project": {
12
+ "type": "string",
13
+ "minLength": 1
14
+ },
15
+ "summary": {
16
+ "type": "object",
17
+ "additionalProperties": false,
18
+ "required": [
19
+ "currentState",
20
+ "implemented",
21
+ "remaining",
22
+ "nextAction"
23
+ ],
24
+ "properties": {
25
+ "currentState": {
26
+ "type": "string",
27
+ "minLength": 1
28
+ },
29
+ "implemented": {
30
+ "type": "array",
31
+ "items": {
32
+ "type": "string"
33
+ }
34
+ },
35
+ "remaining": {
36
+ "type": "array",
37
+ "items": {
38
+ "type": "string"
39
+ }
40
+ },
41
+ "nextAction": {
42
+ "type": "string",
43
+ "minLength": 1
44
+ }
45
+ }
46
+ },
47
+ "constraints": {
48
+ "type": "array",
49
+ "items": {
50
+ "type": "string"
51
+ }
52
+ },
53
+ "tasks": {
54
+ "type": "array",
55
+ "minItems": 1,
56
+ "items": {
57
+ "type": "object",
58
+ "additionalProperties": false,
59
+ "required": [
60
+ "id",
61
+ "title",
62
+ "status",
63
+ "priority",
64
+ "risk",
65
+ "goal",
66
+ "docs",
67
+ "paths",
68
+ "acceptance"
69
+ ],
70
+ "properties": {
71
+ "id": {
72
+ "type": "string",
73
+ "minLength": 1
74
+ },
75
+ "title": {
76
+ "type": "string",
77
+ "minLength": 1
78
+ },
79
+ "status": {
80
+ "type": "string",
81
+ "enum": [
82
+ "pending",
83
+ "done",
84
+ "blocked"
85
+ ]
86
+ },
87
+ "priority": {
88
+ "type": "string",
89
+ "enum": [
90
+ "P0",
91
+ "P1",
92
+ "P2",
93
+ "P3"
94
+ ]
95
+ },
96
+ "risk": {
97
+ "type": "string",
98
+ "enum": [
99
+ "low",
100
+ "medium",
101
+ "high",
102
+ "critical"
103
+ ]
104
+ },
105
+ "goal": {
106
+ "type": "string",
107
+ "minLength": 1
108
+ },
109
+ "docs": {
110
+ "type": "array",
111
+ "items": {
112
+ "type": "string"
113
+ }
114
+ },
115
+ "paths": {
116
+ "type": "array",
117
+ "items": {
118
+ "type": "string"
119
+ }
120
+ },
121
+ "acceptance": {
122
+ "type": "array",
123
+ "minItems": 1,
124
+ "items": {
125
+ "type": "string"
126
+ }
127
+ },
128
+ "dependsOn": {
129
+ "type": "array",
130
+ "items": {
131
+ "type": "string"
132
+ }
133
+ },
134
+ "verify": {
135
+ "type": "array",
136
+ "items": {
137
+ "type": "string"
138
+ }
139
+ }
140
+ }
141
+ }
142
+ }
143
+ }
144
+ }
@@ -15,4 +15,4 @@
15
15
  "message": "HelloLoop 已初始化。",
16
16
  "updatedAt": "2026-03-27T00:00:00.000Z"
17
17
  }
18
-
18
+
File without changes