apphx 0.0.0-stage → 1.0.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.
- package/LICENSE +21 -0
- package/README.md +181 -2
- package/dist/apple-types.js +45 -0
- package/dist/apple-types.js.map +1 -0
- package/dist/apple.js +900 -0
- package/dist/apple.js.map +1 -0
- package/dist/cli.js +414 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.js +80 -0
- package/dist/config.js.map +1 -0
- package/dist/dependencies.js +157 -0
- package/dist/dependencies.js.map +1 -0
- package/dist/export-options.js +49 -0
- package/dist/export-options.js.map +1 -0
- package/dist/flutter.js +171 -0
- package/dist/flutter.js.map +1 -0
- package/dist/inbox.js +128 -0
- package/dist/inbox.js.map +1 -0
- package/dist/integrations.js +435 -0
- package/dist/integrations.js.map +1 -0
- package/dist/preflight.js +43 -0
- package/dist/preflight.js.map +1 -0
- package/dist/process.js +260 -0
- package/dist/process.js.map +1 -0
- package/dist/remote.js +143 -0
- package/dist/remote.js.map +1 -0
- package/dist/requirements.js +37 -0
- package/dist/requirements.js.map +1 -0
- package/dist/runner.js +722 -0
- package/dist/runner.js.map +1 -0
- package/dist/scheduler.js +83 -0
- package/dist/scheduler.js.map +1 -0
- package/dist/schemas.js +232 -0
- package/dist/schemas.js.map +1 -0
- package/dist/store.js +361 -0
- package/dist/store.js.map +1 -0
- package/examples/factory.json +28 -0
- package/examples/luggage.json +39 -0
- package/examples/remote.json +6 -0
- package/package.json +30 -3
- package/skills/apple-app-factory/LICENSE +21 -0
- package/skills/apple-app-factory/SKILL.md +103 -0
- package/skills/apple-app-factory/resources/requirement.json +39 -0
- package/skills/apple-app-factory/resources/research-result.json +21 -0
- package/skills/apple-app-factory/resources/review-result.json +7 -0
package/dist/store.js
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import { ConfigSchema, InboxEntrySchema, JobSchema, SpecSchema, toError } from "./schemas.js";
|
|
2
|
+
import { TERMINATION_GRACE_MS, inspectProcessGroup, signalOwnedProcessGroup } from "./process.js";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { chmodSync, mkdirSync } from "node:fs";
|
|
6
|
+
import { DatabaseSync } from "node:sqlite";
|
|
7
|
+
// src/store.ts
|
|
8
|
+
function parseStoredJob(data) {
|
|
9
|
+
if (typeof data !== "string") throw new Error("Stored task data must be JSON text");
|
|
10
|
+
return JobSchema.parse(JSON.parse(data));
|
|
11
|
+
}
|
|
12
|
+
var Store = class {
|
|
13
|
+
db;
|
|
14
|
+
constructor(directory) {
|
|
15
|
+
mkdirSync(directory, {
|
|
16
|
+
recursive: true,
|
|
17
|
+
mode: 448
|
|
18
|
+
});
|
|
19
|
+
this.db = new DatabaseSync(join(directory, "queue.sqlite"));
|
|
20
|
+
chmodSync(join(directory, "queue.sqlite"), 384);
|
|
21
|
+
this.db.exec(`PRAGMA busy_timeout=5000; PRAGMA journal_mode=WAL;
|
|
22
|
+
CREATE TABLE IF NOT EXISTS jobs (id TEXT PRIMARY KEY, product TEXT NOT NULL, version TEXT NOT NULL, status TEXT NOT NULL, next_at INTEGER NOT NULL, data TEXT NOT NULL, UNIQUE(product, version));
|
|
23
|
+
CREATE TABLE IF NOT EXISTS events (seq INTEGER PRIMARY KEY, job TEXT NOT NULL, at INTEGER NOT NULL, data TEXT NOT NULL);
|
|
24
|
+
CREATE TABLE IF NOT EXISTS worker (id INTEGER PRIMARY KEY CHECK(id=1), pid INTEGER NOT NULL);
|
|
25
|
+
CREATE TABLE IF NOT EXISTS inbox (source TEXT PRIMARY KEY, digest TEXT, data TEXT NOT NULL);`);
|
|
26
|
+
const version = Number(this.db.prepare("PRAGMA user_version").get()?.user_version);
|
|
27
|
+
if (version > 2) {
|
|
28
|
+
this.db.close();
|
|
29
|
+
throw new Error(`Queue schema ${version} is newer than this factory supports; use the version that created it. No task data was changed.`);
|
|
30
|
+
}
|
|
31
|
+
if (version < 2) try {
|
|
32
|
+
this.transaction(() => {
|
|
33
|
+
const owner = this.db.prepare("SELECT pid FROM worker WHERE id=1").get();
|
|
34
|
+
if (owner) {
|
|
35
|
+
if (typeof owner.pid !== "number" || !Number.isSafeInteger(owner.pid) || owner.pid <= 1) throw new Error("Legacy queue has invalid execution ownership; inspect it with the previous factory version");
|
|
36
|
+
let alive = true;
|
|
37
|
+
try {
|
|
38
|
+
process.kill(owner.pid, 0);
|
|
39
|
+
} catch (cause) {
|
|
40
|
+
alive = toError(cause).code !== "ESRCH";
|
|
41
|
+
}
|
|
42
|
+
if (alive) throw new Error(`Stop the previous factory execution (${owner.pid}) before migrating its queue; no process was signaled`);
|
|
43
|
+
}
|
|
44
|
+
this.db.exec("CREATE TABLE IF NOT EXISTS snapshot_migrations (job TEXT NOT NULL, version INTEGER NOT NULL, data TEXT NOT NULL, PRIMARY KEY(job,version));");
|
|
45
|
+
for (const row of this.db.prepare("SELECT id,data FROM jobs").all()) {
|
|
46
|
+
if (typeof row.data !== "string") throw new Error("Stored task data must be JSON text");
|
|
47
|
+
const saved = JSON.parse(row.data);
|
|
48
|
+
this.db.prepare("INSERT OR IGNORE INTO snapshot_migrations VALUES(?,?,?)").run(row.id, version, row.data);
|
|
49
|
+
if (saved.requestSpec === void 0) {
|
|
50
|
+
const changed = this.db.prepare("SELECT 1 FROM events WHERE job=? AND json_type(data,'$.spec') IS NOT NULL LIMIT 1").get(row.id);
|
|
51
|
+
saved.requestSpec = changed ? null : saved.spec;
|
|
52
|
+
saved.requestSpecError = changed ? "Original intake identity cannot be proven after legacy requirement edits; use reconcile-intake with the original requirements" : null;
|
|
53
|
+
}
|
|
54
|
+
const config = saved.config;
|
|
55
|
+
config.executionEnv ??= config.agentEnv ?? [];
|
|
56
|
+
config.maxHostTurns ??= config.maxAgentCalls ?? 16;
|
|
57
|
+
for (const key of [
|
|
58
|
+
"builder",
|
|
59
|
+
"reviewer",
|
|
60
|
+
"researcher",
|
|
61
|
+
"agentEnv",
|
|
62
|
+
"maxAgentCalls",
|
|
63
|
+
"maxConcurrentJobs"
|
|
64
|
+
]) delete config[key];
|
|
65
|
+
saved.hostTurns ??= saved.agentCalls ?? 0;
|
|
66
|
+
delete saved.agentCalls;
|
|
67
|
+
const job = JobSchema.parse(saved);
|
|
68
|
+
this.db.prepare("UPDATE jobs SET data=? WHERE id=?").run(JSON.stringify(job), job.id);
|
|
69
|
+
}
|
|
70
|
+
this.db.exec("PRAGMA user_version=2;");
|
|
71
|
+
});
|
|
72
|
+
} catch (cause) {
|
|
73
|
+
this.db.close();
|
|
74
|
+
throw new Error(`Cannot migrate saved factory snapshots: ${toError(cause).message}. No migration was committed. Preserve queue.sqlite and use the previous factory version to inspect or repair unsupported data.`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
close() {
|
|
78
|
+
this.db.close();
|
|
79
|
+
}
|
|
80
|
+
transaction(action) {
|
|
81
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
82
|
+
try {
|
|
83
|
+
const result = action();
|
|
84
|
+
this.db.exec("COMMIT");
|
|
85
|
+
return result;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
this.db.exec("ROLLBACK");
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
list() {
|
|
92
|
+
return this.db.prepare("SELECT data FROM jobs ORDER BY rowid").all().map((row) => parseStoredJob(row.data));
|
|
93
|
+
}
|
|
94
|
+
get(id) {
|
|
95
|
+
const row = this.db.prepare("SELECT data FROM jobs WHERE id=?").get(id);
|
|
96
|
+
if (!row) throw new Error(`Unknown task ${id}; use status to list task IDs`);
|
|
97
|
+
return parseStoredJob(row.data);
|
|
98
|
+
}
|
|
99
|
+
enqueue(specValue, configValue, baseJobId) {
|
|
100
|
+
const spec = SpecSchema.parse(specValue);
|
|
101
|
+
const config = ConfigSchema.parse(configValue);
|
|
102
|
+
return this.transaction(() => this.insertJob(spec, config, baseJobId));
|
|
103
|
+
}
|
|
104
|
+
checkIngest(specValue, baseJobId, pending = []) {
|
|
105
|
+
const spec = SpecSchema.parse(specValue);
|
|
106
|
+
const row = this.db.prepare("SELECT data FROM jobs WHERE product=? AND version=?").get(spec.id, spec.apple?.version ?? "1.0.0");
|
|
107
|
+
const existing = row ? parseStoredJob(row.data) : void 0;
|
|
108
|
+
if (existing?.requestSpec === null) throw new Error(existing.requestSpecError ?? "Original intake identity is unknown; use reconcile-intake with the original requirements");
|
|
109
|
+
const prospective = pending.find((item) => item.spec.id === spec.id && (item.spec.apple?.version ?? "1.0.0") === (spec.apple?.version ?? "1.0.0"));
|
|
110
|
+
const original = existing?.requestSpec ? {
|
|
111
|
+
spec: existing.requestSpec,
|
|
112
|
+
baseJobId: existing.baseJobId
|
|
113
|
+
} : void 0;
|
|
114
|
+
for (const candidate of [original, prospective]) if (candidate && (JSON.stringify(candidate.spec) !== JSON.stringify(spec) || candidate.baseJobId !== baseJobId)) throw new Error(`${spec.id} already has immutable requirements for this version; submit a new apple.version instead of editing its task`);
|
|
115
|
+
if (existing) return existing;
|
|
116
|
+
const signature = (value) => `${value.purpose.trim().toLowerCase()}\n${value.audience.trim().toLowerCase()}`;
|
|
117
|
+
const duplicates = (job) => job.spec.id !== spec.id && signature(job.spec) === signature(spec);
|
|
118
|
+
const duplicate = this.list().find(duplicates) ?? pending.find(duplicates);
|
|
119
|
+
if (duplicate) throw new Error(`Product purpose and audience duplicate ${duplicate.spec.id}; differentiate actual utility, not names or code`);
|
|
120
|
+
const base = baseJobId === void 0 ? void 0 : this.get(baseJobId);
|
|
121
|
+
if (base && (base.spec.id !== spec.id || ![
|
|
122
|
+
"built",
|
|
123
|
+
"ready",
|
|
124
|
+
"approved",
|
|
125
|
+
"released"
|
|
126
|
+
].includes(base.status) || !base.verifiedDigest || base.groups.length)) throw new Error("baseJobId must identify a finished, verified task of the same product with no live command groups");
|
|
127
|
+
}
|
|
128
|
+
insertJob(spec, config, baseJobId) {
|
|
129
|
+
if (this.checkIngest(spec, baseJobId)) throw new Error(`${spec.id} ${spec.apple?.version ?? "1.0.0"} is already queued; retry its task or use a new product version`);
|
|
130
|
+
const version = spec.apple?.version ?? "1.0.0";
|
|
131
|
+
const base = baseJobId === void 0 ? void 0 : this.get(baseJobId);
|
|
132
|
+
const now = Date.now();
|
|
133
|
+
const id = randomUUID();
|
|
134
|
+
const job = JobSchema.parse({
|
|
135
|
+
id,
|
|
136
|
+
spec,
|
|
137
|
+
requestSpec: spec,
|
|
138
|
+
config,
|
|
139
|
+
...base ? {
|
|
140
|
+
baseJobId: base.id,
|
|
141
|
+
baseDigest: base.verifiedDigest
|
|
142
|
+
} : {},
|
|
143
|
+
stage: "research",
|
|
144
|
+
status: "queued",
|
|
145
|
+
blockedReason: null,
|
|
146
|
+
manualRecovery: null,
|
|
147
|
+
createdAt: now,
|
|
148
|
+
updatedAt: now,
|
|
149
|
+
nextAt: now,
|
|
150
|
+
attempts: {},
|
|
151
|
+
hostTurns: 0,
|
|
152
|
+
turn: null,
|
|
153
|
+
groups: [],
|
|
154
|
+
commands: {},
|
|
155
|
+
receipt: {},
|
|
156
|
+
logs: [],
|
|
157
|
+
workspace: join(config.workspaceRoot, id)
|
|
158
|
+
});
|
|
159
|
+
this.db.prepare("INSERT INTO jobs VALUES (?,?,?,?,?,?)").run(id, spec.id, version, job.status, now, JSON.stringify(job));
|
|
160
|
+
this.event(id, {
|
|
161
|
+
stage: job.stage,
|
|
162
|
+
status: job.status,
|
|
163
|
+
baseJobId
|
|
164
|
+
});
|
|
165
|
+
return job;
|
|
166
|
+
}
|
|
167
|
+
inboxEntries() {
|
|
168
|
+
return this.db.prepare("SELECT data FROM inbox ORDER BY source").all().map((row) => {
|
|
169
|
+
if (typeof row.data !== "string") throw new Error("Stored inbox data must be JSON text");
|
|
170
|
+
return InboxEntrySchema.parse(JSON.parse(row.data));
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
saveInbox(entry) {
|
|
174
|
+
this.db.prepare("INSERT INTO inbox VALUES(?,?,?) ON CONFLICT(source) DO UPDATE SET digest=excluded.digest,data=excluded.data").run(entry.source, entry.digest, JSON.stringify(entry));
|
|
175
|
+
return entry;
|
|
176
|
+
}
|
|
177
|
+
inboxError(source, digest, error) {
|
|
178
|
+
const previous = this.inboxEntries().find((entry) => entry.source === source);
|
|
179
|
+
if (previous?.status === "error" && previous.digest === digest && previous.error === error) return previous;
|
|
180
|
+
return this.saveInbox(InboxEntrySchema.parse({
|
|
181
|
+
source,
|
|
182
|
+
digest,
|
|
183
|
+
status: "error",
|
|
184
|
+
jobId: previous?.jobId ?? null,
|
|
185
|
+
error,
|
|
186
|
+
observedAt: Date.now()
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
ingest(source, digest, specValue, configValue, baseJobId) {
|
|
190
|
+
const spec = SpecSchema.parse(specValue);
|
|
191
|
+
const config = ConfigSchema.parse(configValue);
|
|
192
|
+
return this.transaction(() => {
|
|
193
|
+
const job = this.checkIngest(spec, baseJobId) ?? this.insertJob(spec, config, baseJobId);
|
|
194
|
+
return this.saveInbox(InboxEntrySchema.parse({
|
|
195
|
+
source,
|
|
196
|
+
digest,
|
|
197
|
+
status: "accepted",
|
|
198
|
+
jobId: job.id,
|
|
199
|
+
error: null,
|
|
200
|
+
observedAt: Date.now()
|
|
201
|
+
}));
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
event(id, data) {
|
|
205
|
+
this.db.prepare("INSERT INTO events(job,at,data) VALUES (?,?,?)").run(id, Date.now(), JSON.stringify(data));
|
|
206
|
+
}
|
|
207
|
+
update(id, patch) {
|
|
208
|
+
return this.transition(id, () => patch);
|
|
209
|
+
}
|
|
210
|
+
transition(id, change) {
|
|
211
|
+
return this.transaction(() => {
|
|
212
|
+
const current = this.get(id);
|
|
213
|
+
return this.saveJob(current, change(current));
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
saveJob(current, patch, reconcileIntake = false) {
|
|
217
|
+
const job = JobSchema.parse({
|
|
218
|
+
...current,
|
|
219
|
+
...patch,
|
|
220
|
+
updatedAt: Date.now()
|
|
221
|
+
});
|
|
222
|
+
if (!reconcileIntake && patch.requestSpec !== void 0 && JSON.stringify(job.requestSpec) !== JSON.stringify(current.requestSpec)) throw new Error("Original intake requirements cannot be changed by task updates");
|
|
223
|
+
this.db.prepare("UPDATE jobs SET status=?,next_at=?,data=? WHERE id=?").run(job.status, job.nextAt, JSON.stringify(job), job.id);
|
|
224
|
+
this.event(job.id, patch);
|
|
225
|
+
return job;
|
|
226
|
+
}
|
|
227
|
+
reconcileIntake(id, specValue, baseJobId) {
|
|
228
|
+
const spec = SpecSchema.parse(specValue);
|
|
229
|
+
return this.transaction(() => {
|
|
230
|
+
const current = this.get(id);
|
|
231
|
+
if (current.requestSpec !== null) throw new Error("This task already has a known intake identity; it cannot be replaced");
|
|
232
|
+
if (spec.id !== current.spec.id || (spec.apple?.version ?? "1.0.0") !== (current.spec.apple?.version ?? "1.0.0") || baseJobId !== current.baseJobId) throw new Error("Original requirements must match this task product, version and predecessor");
|
|
233
|
+
return this.saveJob(current, {
|
|
234
|
+
requestSpec: spec,
|
|
235
|
+
requestSpecError: null
|
|
236
|
+
}, true);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
acquire() {
|
|
240
|
+
const jobs = this.transaction(() => {
|
|
241
|
+
const jobs = this.list();
|
|
242
|
+
const owner = this.db.prepare("SELECT pid FROM worker WHERE id=1").get();
|
|
243
|
+
if (owner) {
|
|
244
|
+
if (typeof owner.pid !== "number" || !Number.isSafeInteger(owner.pid) || owner.pid <= 1) throw new Error("Stored worker PID must be a valid process ID");
|
|
245
|
+
let alive = true;
|
|
246
|
+
try {
|
|
247
|
+
process.kill(owner.pid, 0);
|
|
248
|
+
} catch (error) {
|
|
249
|
+
alive = toError(error).code !== "ESRCH";
|
|
250
|
+
}
|
|
251
|
+
if (alive) throw new Error(`Execution ${owner.pid} already owns this queue; wait for its command to finish`);
|
|
252
|
+
}
|
|
253
|
+
this.db.prepare("INSERT OR REPLACE INTO worker VALUES(1,?)").run(process.pid);
|
|
254
|
+
return jobs;
|
|
255
|
+
});
|
|
256
|
+
for (const job of jobs) if (job.status === "running" && !job.turn) this.update(job.id, {
|
|
257
|
+
status: "waiting",
|
|
258
|
+
blockedReason: null,
|
|
259
|
+
nextAt: Date.now(),
|
|
260
|
+
error: "Execution stopped during a deterministic stage; recover its command groups before resuming"
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
release() {
|
|
264
|
+
this.transaction(() => {
|
|
265
|
+
this.db.prepare("DELETE FROM worker WHERE id=1 AND pid=?").run(process.pid);
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
next(canClaim = () => true, excludeIds = /* @__PURE__ */ new Set()) {
|
|
269
|
+
const now = Date.now();
|
|
270
|
+
const jobs = this.db.prepare("SELECT data FROM jobs WHERE (status IN ('queued','waiting') OR (status='blocked' AND json_extract(data,'$.blockedReason.kind')='prerequisite')) AND next_at<=? ORDER BY next_at,rowid").all(now).map((row) => parseStoredJob(row.data));
|
|
271
|
+
for (const job of jobs) {
|
|
272
|
+
if (excludeIds.has(job.id)) continue;
|
|
273
|
+
const groups = [], commands = {};
|
|
274
|
+
let manualRecovery = job.manualRecovery ?? null, pending = false, nextAt = Infinity;
|
|
275
|
+
for (const pid of job.groups) {
|
|
276
|
+
const command = job.commands[pid];
|
|
277
|
+
const ownership = inspectProcessGroup(pid, command?.identity);
|
|
278
|
+
if (ownership === "gone") continue;
|
|
279
|
+
groups.push(pid);
|
|
280
|
+
const saved = command ? { ...command } : void 0;
|
|
281
|
+
if (saved) commands[pid] = saved;
|
|
282
|
+
if (ownership !== "owned" || !saved || !Number.isFinite(saved.deadline)) {
|
|
283
|
+
manualRecovery ??= `Command group ${pid} has ${ownership === "mismatch" ? "a different process identity" : "no verifiable ownership and deadline"}. It was not signaled. Inspect this task's recorded command ownership before retrying.`;
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
if (!saved.termAt && now < saved.deadline) nextAt = Math.min(nextAt, saved.deadline, now + job.config.pollMs);
|
|
288
|
+
else if (!saved.termAt) {
|
|
289
|
+
signalOwnedProcessGroup(pid, saved.identity, "SIGTERM");
|
|
290
|
+
saved.termAt = Date.now();
|
|
291
|
+
nextAt = Math.min(nextAt, saved.termAt + TERMINATION_GRACE_MS);
|
|
292
|
+
} else if (now < saved.termAt + 1e3) nextAt = Math.min(nextAt, saved.termAt + TERMINATION_GRACE_MS);
|
|
293
|
+
else if (!saved.killAt) {
|
|
294
|
+
signalOwnedProcessGroup(pid, saved.identity, "SIGKILL");
|
|
295
|
+
saved.killAt = Date.now();
|
|
296
|
+
nextAt = Math.min(nextAt, saved.killAt + TERMINATION_GRACE_MS);
|
|
297
|
+
} else if (now < saved.killAt + 1e3) nextAt = Math.min(nextAt, saved.killAt + TERMINATION_GRACE_MS);
|
|
298
|
+
else {
|
|
299
|
+
manualRecovery ??= `Command group ${pid} is still active after SIGKILL. No replay is allowed. Inspect and stop the recorded group, then retry this task.`;
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
pending = true;
|
|
303
|
+
} catch (error) {
|
|
304
|
+
manualRecovery ??= toError(error).message;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (!groups.length && manualRecovery === null) {
|
|
308
|
+
if (job.groups.length) this.update(job.id, {
|
|
309
|
+
groups: [],
|
|
310
|
+
commands: {},
|
|
311
|
+
recoveryStage: void 0
|
|
312
|
+
});
|
|
313
|
+
const claimed = this.transaction(() => {
|
|
314
|
+
const current = this.get(job.id);
|
|
315
|
+
if (!(current.status === "queued" || current.status === "waiting" || current.status === "blocked" && current.blockedReason?.kind === "prerequisite") || current.turn || current.nextAt > Date.now() || current.groups.length || current.manualRecovery != null || !canClaim(current)) return void 0;
|
|
316
|
+
return this.saveJob(current, {
|
|
317
|
+
status: "running",
|
|
318
|
+
blockedReason: null,
|
|
319
|
+
manualRecovery: null,
|
|
320
|
+
groups: [],
|
|
321
|
+
commands: {},
|
|
322
|
+
recoveryStage: void 0
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
if (claimed) return claimed;
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (pending) this.update(job.id, {
|
|
329
|
+
status: "waiting",
|
|
330
|
+
blockedReason: null,
|
|
331
|
+
manualRecovery,
|
|
332
|
+
groups,
|
|
333
|
+
commands,
|
|
334
|
+
nextAt,
|
|
335
|
+
error: manualRecovery ?? "Recovering an owned command group; waiting for its deadline and TERM-to-KILL cleanup before replay"
|
|
336
|
+
});
|
|
337
|
+
else this.update(job.id, {
|
|
338
|
+
status: "blocked",
|
|
339
|
+
blockedReason: null,
|
|
340
|
+
manualRecovery,
|
|
341
|
+
groups,
|
|
342
|
+
commands,
|
|
343
|
+
error: manualRecovery
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
events(id) {
|
|
348
|
+
this.get(id);
|
|
349
|
+
return this.db.prepare("SELECT seq,at,data FROM events WHERE job=? ORDER BY seq").all(id).map((row) => {
|
|
350
|
+
if (typeof row.data !== "string") throw new Error("Stored event data must be JSON text");
|
|
351
|
+
const data = JSON.parse(row.data);
|
|
352
|
+
return {
|
|
353
|
+
...row,
|
|
354
|
+
data
|
|
355
|
+
};
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
export { Store };
|
|
360
|
+
|
|
361
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","names":[],"sources":["../src/store.ts"],"sourcesContent":["import { DatabaseSync } from 'node:sqlite';\nimport { mkdirSync, chmodSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { randomUUID } from 'node:crypto';\nimport { inspectProcessGroup, signalOwnedProcessGroup, TERMINATION_GRACE_MS } from './process.js';\nimport { ConfigSchema, JobSchema, SpecSchema, InboxEntrySchema, toError, type CommandMetadata, type Job, type Spec, type Config, type InboxEntry } from './schemas.js';\n\nfunction parseStoredJob(data: unknown): Job {\n if (typeof data !== 'string') throw new Error('Stored task data must be JSON text');\n return JobSchema.parse(JSON.parse(data));\n}\n\nexport class Store {\n readonly db: DatabaseSync;\n\n constructor(directory: string) {\n mkdirSync(directory, { recursive: true, mode: 0o700 });\n this.db = new DatabaseSync(join(directory, 'queue.sqlite'));\n chmodSync(join(directory, 'queue.sqlite'), 0o600);\n this.db.exec(`PRAGMA busy_timeout=5000; PRAGMA journal_mode=WAL;\n CREATE TABLE IF NOT EXISTS jobs (id TEXT PRIMARY KEY, product TEXT NOT NULL, version TEXT NOT NULL, status TEXT NOT NULL, next_at INTEGER NOT NULL, data TEXT NOT NULL, UNIQUE(product, version));\n CREATE TABLE IF NOT EXISTS events (seq INTEGER PRIMARY KEY, job TEXT NOT NULL, at INTEGER NOT NULL, data TEXT NOT NULL);\n CREATE TABLE IF NOT EXISTS worker (id INTEGER PRIMARY KEY CHECK(id=1), pid INTEGER NOT NULL);\n CREATE TABLE IF NOT EXISTS inbox (source TEXT PRIMARY KEY, digest TEXT, data TEXT NOT NULL);`);\n const version = Number(this.db.prepare('PRAGMA user_version').get()?.user_version);\n if (version > 2) { this.db.close(); throw new Error(`Queue schema ${version} is newer than this factory supports; use the version that created it. No task data was changed.`); }\n if (version < 2) {\n try {\n this.transaction(() => {\n const owner = this.db.prepare('SELECT pid FROM worker WHERE id=1').get();\n if (owner) {\n if (typeof owner.pid !== 'number' || !Number.isSafeInteger(owner.pid) || owner.pid <= 1) throw new Error('Legacy queue has invalid execution ownership; inspect it with the previous factory version');\n let alive = true;\n try { process.kill(owner.pid, 0); } catch (cause) { alive = toError(cause).code !== 'ESRCH'; }\n if (alive) throw new Error(`Stop the previous factory execution (${owner.pid}) before migrating its queue; no process was signaled`);\n }\n this.db.exec('CREATE TABLE IF NOT EXISTS snapshot_migrations (job TEXT NOT NULL, version INTEGER NOT NULL, data TEXT NOT NULL, PRIMARY KEY(job,version));');\n for (const row of this.db.prepare('SELECT id,data FROM jobs').all()) {\n if (typeof row.data !== 'string') throw new Error('Stored task data must be JSON text');\n const saved = JSON.parse(row.data);\n this.db.prepare('INSERT OR IGNORE INTO snapshot_migrations VALUES(?,?,?)').run(row.id, version, row.data);\n if (saved.requestSpec === undefined) {\n const changed = this.db.prepare(\"SELECT 1 FROM events WHERE job=? AND json_type(data,'$.spec') IS NOT NULL LIMIT 1\").get(row.id);\n saved.requestSpec = changed ? null : saved.spec;\n saved.requestSpecError = changed ? 'Original intake identity cannot be proven after legacy requirement edits; use reconcile-intake with the original requirements' : null;\n }\n const config = saved.config;\n config.executionEnv ??= config.agentEnv ?? [];\n config.maxHostTurns ??= config.maxAgentCalls ?? 16;\n for (const key of ['builder', 'reviewer', 'researcher', 'agentEnv', 'maxAgentCalls', 'maxConcurrentJobs']) delete config[key];\n saved.hostTurns ??= saved.agentCalls ?? 0;\n delete saved.agentCalls;\n const job = JobSchema.parse(saved);\n this.db.prepare('UPDATE jobs SET data=? WHERE id=?').run(JSON.stringify(job), job.id);\n }\n this.db.exec('PRAGMA user_version=2;');\n });\n } catch (cause) {\n this.db.close();\n throw new Error(`Cannot migrate saved factory snapshots: ${toError(cause).message}. No migration was committed. Preserve queue.sqlite and use the previous factory version to inspect or repair unsupported data.`);\n }\n }\n }\n close(): void { this.db.close(); }\n transaction<T>(action: () => T): T {\n this.db.exec('BEGIN IMMEDIATE');\n try { const result = action(); this.db.exec('COMMIT'); return result; }\n catch (error) { this.db.exec('ROLLBACK'); throw error; }\n }\n list(): Job[] { return this.db.prepare('SELECT data FROM jobs ORDER BY rowid').all().map(row => parseStoredJob(row.data)); }\n get(id: string): Job {\n const row = this.db.prepare('SELECT data FROM jobs WHERE id=?').get(id);\n if (!row) throw new Error(`Unknown task ${id}; use status to list task IDs`);\n return parseStoredJob(row.data);\n }\n enqueue(specValue: unknown, configValue: unknown, baseJobId?: string): Job {\n const spec = SpecSchema.parse(specValue);\n const config = ConfigSchema.parse(configValue);\n return this.transaction(() => this.insertJob(spec, config, baseJobId));\n }\n checkIngest(specValue: unknown, baseJobId?: string, pending: ReadonlyArray<{ spec: Spec; baseJobId?: string }> = []): Job | undefined {\n const spec = SpecSchema.parse(specValue);\n const row = this.db.prepare('SELECT data FROM jobs WHERE product=? AND version=?').get(spec.id, spec.apple?.version ?? '1.0.0');\n const existing = row ? parseStoredJob(row.data) : undefined;\n if (existing?.requestSpec === null) throw new Error(existing.requestSpecError ?? 'Original intake identity is unknown; use reconcile-intake with the original requirements');\n const prospective = pending.find(item => item.spec.id === spec.id && (item.spec.apple?.version ?? '1.0.0') === (spec.apple?.version ?? '1.0.0'));\n const original = existing?.requestSpec ? { spec: existing.requestSpec, baseJobId: existing.baseJobId } : undefined;\n for (const candidate of [original, prospective]) {\n if (candidate && (JSON.stringify(candidate.spec) !== JSON.stringify(spec) || candidate.baseJobId !== baseJobId)) throw new Error(`${spec.id} already has immutable requirements for this version; submit a new apple.version instead of editing its task`);\n }\n if (existing) return existing;\n const signature = (value: { purpose: string; audience: string }): string => `${value.purpose.trim().toLowerCase()}\\n${value.audience.trim().toLowerCase()}`;\n const duplicates = (job: { spec: Spec }): boolean => job.spec.id !== spec.id && signature(job.spec) === signature(spec);\n const duplicate = this.list().find(duplicates) ?? pending.find(duplicates);\n if (duplicate) throw new Error(`Product purpose and audience duplicate ${duplicate.spec.id}; differentiate actual utility, not names or code`);\n const base = baseJobId === undefined ? undefined : this.get(baseJobId);\n if (base && (base.spec.id !== spec.id || !['built', 'ready', 'approved', 'released'].includes(base.status) || !base.verifiedDigest || base.groups.length)) {\n throw new Error('baseJobId must identify a finished, verified task of the same product with no live command groups');\n }\n return undefined;\n }\n private insertJob(spec: Spec, config: Config, baseJobId?: string): Job {\n if (this.checkIngest(spec, baseJobId)) throw new Error(`${spec.id} ${spec.apple?.version ?? '1.0.0'} is already queued; retry its task or use a new product version`);\n const version = spec.apple?.version ?? '1.0.0';\n const base = baseJobId === undefined ? undefined : this.get(baseJobId);\n const now = Date.now();\n const id = randomUUID();\n const job = JobSchema.parse({ id, spec, requestSpec: spec, config, ...(base ? { baseJobId: base.id, baseDigest: base.verifiedDigest } : {}), stage: 'research', status: 'queued', blockedReason: null, manualRecovery: null, createdAt: now, updatedAt: now, nextAt: now, attempts: {}, hostTurns: 0, turn: null, groups: [], commands: {}, receipt: {}, logs: [], workspace: join(config.workspaceRoot, id) });\n this.db.prepare('INSERT INTO jobs VALUES (?,?,?,?,?,?)').run(id, spec.id, version, job.status, now, JSON.stringify(job));\n this.event(id, { stage: job.stage, status: job.status, baseJobId });\n return job;\n }\n inboxEntries(): InboxEntry[] {\n return this.db.prepare('SELECT data FROM inbox ORDER BY source').all().map(row => {\n if (typeof row.data !== 'string') throw new Error('Stored inbox data must be JSON text');\n return InboxEntrySchema.parse(JSON.parse(row.data));\n });\n }\n private saveInbox(entry: InboxEntry): InboxEntry {\n this.db.prepare('INSERT INTO inbox VALUES(?,?,?) ON CONFLICT(source) DO UPDATE SET digest=excluded.digest,data=excluded.data').run(entry.source, entry.digest, JSON.stringify(entry));\n return entry;\n }\n inboxError(source: string, digest: string | null, error: string): InboxEntry {\n const previous = this.inboxEntries().find(entry => entry.source === source);\n if (previous?.status === 'error' && previous.digest === digest && previous.error === error) return previous;\n return this.saveInbox(InboxEntrySchema.parse({ source, digest, status: 'error', jobId: previous?.jobId ?? null, error, observedAt: Date.now() }));\n }\n ingest(source: string, digest: string, specValue: unknown, configValue: unknown, baseJobId?: string): InboxEntry {\n const spec = SpecSchema.parse(specValue);\n const config = ConfigSchema.parse(configValue);\n return this.transaction(() => {\n const job = this.checkIngest(spec, baseJobId) ?? this.insertJob(spec, config, baseJobId);\n return this.saveInbox(InboxEntrySchema.parse({ source, digest, status: 'accepted', jobId: job.id, error: null, observedAt: Date.now() }));\n });\n }\n event(id: string, data: Record<string, unknown>): void { this.db.prepare('INSERT INTO events(job,at,data) VALUES (?,?,?)').run(id, Date.now(), JSON.stringify(data)); }\n update(id: string, patch: Partial<Job>): Job {\n return this.transition(id, () => patch);\n }\n transition(id: string, change: (current: Job) => Partial<Job>): Job {\n return this.transaction(() => {\n const current = this.get(id);\n return this.saveJob(current, change(current));\n });\n }\n private saveJob(current: Job, patch: Partial<Job>, reconcileIntake = false): Job {\n const job = JobSchema.parse({ ...current, ...patch, updatedAt: Date.now() });\n if (!reconcileIntake && patch.requestSpec !== undefined && JSON.stringify(job.requestSpec) !== JSON.stringify(current.requestSpec)) throw new Error('Original intake requirements cannot be changed by task updates');\n this.db.prepare('UPDATE jobs SET status=?,next_at=?,data=? WHERE id=?').run(job.status, job.nextAt, JSON.stringify(job), job.id);\n this.event(job.id, patch);\n return job;\n }\n reconcileIntake(id: string, specValue: unknown, baseJobId?: string): Job {\n const spec = SpecSchema.parse(specValue);\n return this.transaction(() => {\n const current = this.get(id);\n if (current.requestSpec !== null) throw new Error('This task already has a known intake identity; it cannot be replaced');\n if (spec.id !== current.spec.id || (spec.apple?.version ?? '1.0.0') !== (current.spec.apple?.version ?? '1.0.0') || baseJobId !== current.baseJobId) throw new Error('Original requirements must match this task product, version and predecessor');\n return this.saveJob(current, { requestSpec: spec, requestSpecError: null }, true);\n });\n }\n acquire(): void {\n const jobs = this.transaction(() => {\n const jobs = this.list();\n const owner = this.db.prepare('SELECT pid FROM worker WHERE id=1').get();\n if (owner) {\n if (typeof owner.pid !== 'number' || !Number.isSafeInteger(owner.pid) || owner.pid <= 1) throw new Error('Stored worker PID must be a valid process ID');\n let alive = true;\n try { process.kill(owner.pid, 0); } catch (error) { alive = toError(error).code !== 'ESRCH'; }\n if (alive) throw new Error(`Execution ${owner.pid} already owns this queue; wait for its command to finish`);\n }\n this.db.prepare('INSERT OR REPLACE INTO worker VALUES(1,?)').run(process.pid);\n return jobs;\n });\n for (const job of jobs) {\n if (job.status === 'running' && !job.turn) this.update(job.id, { status: 'waiting', blockedReason: null, nextAt: Date.now(), error: 'Execution stopped during a deterministic stage; recover its command groups before resuming' });\n }\n }\n release(): void {\n this.transaction(() => {\n this.db.prepare('DELETE FROM worker WHERE id=1 AND pid=?').run(process.pid);\n });\n }\n next(canClaim: (job: Job) => boolean = () => true, excludeIds: ReadonlySet<string> = new Set()): Job | undefined {\n const now = Date.now();\n const rows = this.db.prepare(\"SELECT data FROM jobs WHERE (status IN ('queued','waiting') OR (status='blocked' AND json_extract(data,'$.blockedReason.kind')='prerequisite')) AND next_at<=? ORDER BY next_at,rowid\").all(now);\n const jobs = rows.map(row => parseStoredJob(row.data));\n for (const job of jobs) {\n if (excludeIds.has(job.id)) continue;\n const groups: number[] = [], commands: Record<string, CommandMetadata> = {};\n let manualRecovery = job.manualRecovery ?? null, pending = false, nextAt = Infinity;\n for (const pid of job.groups) {\n const command = job.commands[pid];\n const ownership = inspectProcessGroup(pid, command?.identity);\n if (ownership === 'gone') continue;\n groups.push(pid);\n const saved = command ? { ...command } : undefined;\n if (saved) commands[pid] = saved;\n if (ownership !== 'owned' || !saved || !Number.isFinite(saved.deadline)) {\n manualRecovery ??= `Command group ${pid} has ${ownership === 'mismatch' ? 'a different process identity' : 'no verifiable ownership and deadline'}. It was not signaled. Inspect this task's recorded command ownership before retrying.`;\n continue;\n }\n try {\n if (!saved.termAt && now < saved.deadline) {\n nextAt = Math.min(nextAt, saved.deadline, now + job.config.pollMs);\n } else if (!saved.termAt) {\n signalOwnedProcessGroup(pid, saved.identity, 'SIGTERM');\n saved.termAt = Date.now();\n nextAt = Math.min(nextAt, saved.termAt + TERMINATION_GRACE_MS);\n } else if (now < saved.termAt + TERMINATION_GRACE_MS) {\n nextAt = Math.min(nextAt, saved.termAt + TERMINATION_GRACE_MS);\n } else if (!saved.killAt) {\n signalOwnedProcessGroup(pid, saved.identity, 'SIGKILL');\n saved.killAt = Date.now();\n nextAt = Math.min(nextAt, saved.killAt + TERMINATION_GRACE_MS);\n } else if (now < saved.killAt + TERMINATION_GRACE_MS) {\n nextAt = Math.min(nextAt, saved.killAt + TERMINATION_GRACE_MS);\n } else {\n manualRecovery ??= `Command group ${pid} is still active after SIGKILL. No replay is allowed. Inspect and stop the recorded group, then retry this task.`;\n continue;\n }\n pending = true;\n } catch (error) { manualRecovery ??= toError(error).message; }\n }\n if (!groups.length && manualRecovery === null) {\n if (job.groups.length) this.update(job.id, { groups: [], commands: {}, recoveryStage: undefined });\n const claimed = this.transaction(() => {\n const current = this.get(job.id);\n const eligible = current.status === 'queued' || current.status === 'waiting' || (current.status === 'blocked' && current.blockedReason?.kind === 'prerequisite');\n if (!eligible || current.turn || current.nextAt > Date.now() || current.groups.length || current.manualRecovery != null || !canClaim(current)) return undefined;\n return this.saveJob(current, { status: 'running', blockedReason: null, manualRecovery: null, groups: [], commands: {}, recoveryStage: undefined });\n });\n if (claimed) return claimed;\n continue;\n }\n if (pending) {\n this.update(job.id, { status: 'waiting', blockedReason: null, manualRecovery, groups, commands, nextAt, error: manualRecovery ?? 'Recovering an owned command group; waiting for its deadline and TERM-to-KILL cleanup before replay' });\n } else {\n this.update(job.id, { status: 'blocked', blockedReason: null, manualRecovery, groups, commands, error: manualRecovery });\n }\n }\n return undefined;\n }\n events(id: string) {\n this.get(id);\n return this.db.prepare('SELECT seq,at,data FROM events WHERE job=? ORDER BY seq').all(id).map(row => {\n if (typeof row.data !== 'string') throw new Error('Stored event data must be JSON text');\n const data: unknown = JSON.parse(row.data);\n return { ...row, data };\n });\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"stateDir": ".factory",
|
|
3
|
+
"workspaceRoot": "workspaces",
|
|
4
|
+
"inbox": { "directory": "requirements", "pollMs": 2000 },
|
|
5
|
+
"allowLocalExecution": false,
|
|
6
|
+
"allowSubmit": false,
|
|
7
|
+
"allowRelease": false,
|
|
8
|
+
"packageTarget": "simulator",
|
|
9
|
+
"executionEnv": ["IOS_DEVICE_ID"],
|
|
10
|
+
"timeoutMs": 900000,
|
|
11
|
+
"maxAttempts": 3,
|
|
12
|
+
"maxHostTurns": 16,
|
|
13
|
+
"maxJobHours": 24,
|
|
14
|
+
"retryDelayMs": 30000,
|
|
15
|
+
"pollMs": 300000,
|
|
16
|
+
"verification": {
|
|
17
|
+
"prerequisites": [["xcodebuild", "-version"]],
|
|
18
|
+
"checks": [
|
|
19
|
+
["dart", "format", "--output=none", "--set-exit-if-changed", "lib", "test", "integration_test", "test_driver"],
|
|
20
|
+
["flutter", "analyze", "--fatal-infos"],
|
|
21
|
+
["flutter", "test"],
|
|
22
|
+
["flutter", "build", "ios", "--simulator", "--debug", "--no-pub"]
|
|
23
|
+
],
|
|
24
|
+
"runtime": [
|
|
25
|
+
["flutter", "drive", "--driver", "test_driver/integration_test.dart", "--target", "integration_test/app_test.dart", "-d", "${IOS_DEVICE_ID}", "--no-pub"]
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "pack-balance",
|
|
3
|
+
"name": "行李配重与打包助手",
|
|
4
|
+
"framework": "flutter",
|
|
5
|
+
"purpose": "离线规划多个行李包的重量,完成出发打包与返程核对,减少遗漏和超重",
|
|
6
|
+
"audience": "需要管理随身包与托运行李的个人旅客",
|
|
7
|
+
"differentiators": [
|
|
8
|
+
"每个行李包独立设置空包重量和用户自定限额,移动物品后即时重算",
|
|
9
|
+
"出发与返程拥有独立核对状态,保留明确标记为已消耗的物品",
|
|
10
|
+
"常用物品库、行程复制和经校验的本地备份恢复串成完整流程"
|
|
11
|
+
],
|
|
12
|
+
"constraints": [
|
|
13
|
+
"Flutter iOS 应用,本地持久化,无账号、服务器、广告和统计 SDK",
|
|
14
|
+
"不预置或宣称提供实时航空公司限额,重量上限由用户确认并填写",
|
|
15
|
+
"重量以整数克存储,显示支持克和千克;数量与单件重量非负,避免浮点累积误差",
|
|
16
|
+
"所有功能对审核与正常用户相同,不按 IP、地区、审核时间或远程开关隐藏功能",
|
|
17
|
+
"隐私入口可直接访问,准确说明本地数据、用户主动导出和删除方式",
|
|
18
|
+
"在 build/screenshots 下保存实际运行截图,禁止拼接不存在的功能或使用真实个人信息"
|
|
19
|
+
],
|
|
20
|
+
"acceptance": [
|
|
21
|
+
"创建带目的地和日期的行程,强制终止并重新启动应用后数据仍完整",
|
|
22
|
+
"同一行程建立随身包与托运行李,分别设置空包重量与上限",
|
|
23
|
+
"添加两个单件 1250 克的物品到空包 500 克的行李,显示总重 3000 克;移动一个物品后两个包各自正确更新",
|
|
24
|
+
"修改数量、单件重量和包限额时即时刷新进度;超重边界有文本提示,不仅依赖颜色",
|
|
25
|
+
"出发打包勾选与返程核对彼此独立,已消耗物品不会被错误计入返程遗漏",
|
|
26
|
+
"从常用物品库创建清单并复制行程,副本修改不改变原行程",
|
|
27
|
+
"导出备份,清空数据后恢复得到相同的行程、行李、物品和核对状态",
|
|
28
|
+
"导入损坏、未知版本或字段非法的备份时给出可操作错误,原数据不被部分覆盖",
|
|
29
|
+
"在无网络、空清单、大字体与 VoiceOver 标签场景下完成创建、分配、勾选和恢复流程",
|
|
30
|
+
"截图来自真实运行页面,隐私说明和应用行为一致,不宣称保证航空公司接受或苹果过审"
|
|
31
|
+
],
|
|
32
|
+
"apple": {
|
|
33
|
+
"bundleId": "com.example.packbalance",
|
|
34
|
+
"version": "1.0.0",
|
|
35
|
+
"buildNumber": "1",
|
|
36
|
+
"locale": "zh-Hans",
|
|
37
|
+
"screenshotsDir": "build/screenshots"
|
|
38
|
+
}
|
|
39
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apphx",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Host-native harness for verified Flutter app delivery",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"preferUnplugged": true,
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=22.18"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"app-factory": "./dist/cli.js"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"skills",
|
|
17
|
+
"examples"
|
|
18
|
+
],
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/daguanren21/apple-app-factory.git",
|
|
22
|
+
"directory": "packages/apple-app-factory"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"yaml": "^2.9.1",
|
|
26
|
+
"zod": "^4.6.7"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsdown",
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"test": "node --test"
|
|
32
|
+
}
|
|
6
33
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Apple App Factory contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|