openworkgraph 0.2.12

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 (178) hide show
  1. package/README.md +64 -0
  2. package/dist/api-image-agent.d.ts +32 -0
  3. package/dist/api-image-agent.js +132 -0
  4. package/dist/api-image-output.d.ts +8 -0
  5. package/dist/api-image-output.js +41 -0
  6. package/dist/api.d.ts +35 -0
  7. package/dist/api.js +153 -0
  8. package/dist/auth.d.ts +58 -0
  9. package/dist/auth.js +273 -0
  10. package/dist/backend/adapter.d.ts +35 -0
  11. package/dist/backend/adapter.js +439 -0
  12. package/dist/backend/capabilities.d.ts +23 -0
  13. package/dist/backend/capabilities.js +75 -0
  14. package/dist/backend/diagnostics.d.ts +9 -0
  15. package/dist/backend/diagnostics.js +80 -0
  16. package/dist/backend/index.d.ts +6 -0
  17. package/dist/backend/index.js +7 -0
  18. package/dist/backend/models.d.ts +15 -0
  19. package/dist/backend/models.js +43 -0
  20. package/dist/backend/paths.d.ts +6 -0
  21. package/dist/backend/paths.js +69 -0
  22. package/dist/backend/stdio.d.ts +66 -0
  23. package/dist/backend/stdio.js +172 -0
  24. package/dist/backend/types.d.ts +90 -0
  25. package/dist/backend/types.js +11 -0
  26. package/dist/backup-api.d.ts +10 -0
  27. package/dist/backup-api.js +40 -0
  28. package/dist/blob-store.d.ts +40 -0
  29. package/dist/blob-store.js +218 -0
  30. package/dist/cli-help.d.ts +1 -0
  31. package/dist/cli-help.js +63 -0
  32. package/dist/cli-output.d.ts +4 -0
  33. package/dist/cli-output.js +130 -0
  34. package/dist/cli.d.ts +2 -0
  35. package/dist/cli.js +284 -0
  36. package/dist/client-pairing.d.ts +8 -0
  37. package/dist/client-pairing.js +30 -0
  38. package/dist/directories.d.ts +14 -0
  39. package/dist/directories.js +34 -0
  40. package/dist/errors.d.ts +12 -0
  41. package/dist/errors.js +7 -0
  42. package/dist/events.d.ts +22 -0
  43. package/dist/events.js +155 -0
  44. package/dist/execution-chains.d.ts +24 -0
  45. package/dist/execution-chains.js +231 -0
  46. package/dist/execution-settings.d.ts +10 -0
  47. package/dist/execution-settings.js +38 -0
  48. package/dist/graph-document-history.d.ts +43 -0
  49. package/dist/graph-document-history.js +275 -0
  50. package/dist/graph-files.d.ts +14 -0
  51. package/dist/graph-files.js +59 -0
  52. package/dist/graph-transfer.d.ts +53 -0
  53. package/dist/graph-transfer.js +501 -0
  54. package/dist/graphs.d.ts +68 -0
  55. package/dist/graphs.js +771 -0
  56. package/dist/history.d.ts +20 -0
  57. package/dist/history.js +61 -0
  58. package/dist/http.d.ts +2 -0
  59. package/dist/http.js +42 -0
  60. package/dist/image-credentials.d.ts +11 -0
  61. package/dist/image-credentials.js +130 -0
  62. package/dist/image-providers.d.ts +51 -0
  63. package/dist/image-providers.js +271 -0
  64. package/dist/input-materialization.d.ts +17 -0
  65. package/dist/input-materialization.js +95 -0
  66. package/dist/inputs.d.ts +74 -0
  67. package/dist/inputs.js +301 -0
  68. package/dist/local-control.d.ts +13 -0
  69. package/dist/local-control.js +37 -0
  70. package/dist/network.d.ts +11 -0
  71. package/dist/network.js +48 -0
  72. package/dist/openai-image-driver.d.ts +42 -0
  73. package/dist/openai-image-driver.js +368 -0
  74. package/dist/operations/backups.d.ts +40 -0
  75. package/dist/operations/backups.js +391 -0
  76. package/dist/operations/capacity.d.ts +7 -0
  77. package/dist/operations/capacity.js +35 -0
  78. package/dist/operations/cli-context.d.ts +7 -0
  79. package/dist/operations/cli-context.js +58 -0
  80. package/dist/operations/daemon.d.ts +36 -0
  81. package/dist/operations/daemon.js +159 -0
  82. package/dist/operations/files.d.ts +12 -0
  83. package/dist/operations/files.js +84 -0
  84. package/dist/operations/lifecycle.d.ts +28 -0
  85. package/dist/operations/lifecycle.js +155 -0
  86. package/dist/operations/local-admin.d.ts +6 -0
  87. package/dist/operations/local-admin.js +38 -0
  88. package/dist/operations/logs.d.ts +16 -0
  89. package/dist/operations/logs.js +37 -0
  90. package/dist/operations/plugins.d.ts +10 -0
  91. package/dist/operations/plugins.js +11 -0
  92. package/dist/operations/snapshot-worker.d.ts +1 -0
  93. package/dist/operations/snapshot-worker.js +18 -0
  94. package/dist/operations/snapshot.d.ts +1 -0
  95. package/dist/operations/snapshot.js +14 -0
  96. package/dist/operations/update.d.ts +26 -0
  97. package/dist/operations/update.js +216 -0
  98. package/dist/persistence/asset-business-schema.d.ts +2 -0
  99. package/dist/persistence/asset-business-schema.js +36 -0
  100. package/dist/persistence/database.d.ts +7 -0
  101. package/dist/persistence/database.js +91 -0
  102. package/dist/persistence/plugin-schema.d.ts +9 -0
  103. package/dist/persistence/plugin-schema.js +38 -0
  104. package/dist/persistence/repositories.d.ts +20 -0
  105. package/dist/persistence/repositories.js +106 -0
  106. package/dist/persistence/resource-schema.d.ts +2 -0
  107. package/dist/persistence/resource-schema.js +44 -0
  108. package/dist/persistence/schema.d.ts +6 -0
  109. package/dist/persistence/schema.js +236 -0
  110. package/dist/persistence/workflow-schema.d.ts +2 -0
  111. package/dist/persistence/workflow-schema.js +19 -0
  112. package/dist/plugin-api.d.ts +10 -0
  113. package/dist/plugin-api.js +40 -0
  114. package/dist/plugin-management.d.ts +36 -0
  115. package/dist/plugin-management.js +112 -0
  116. package/dist/plugins.d.ts +55 -0
  117. package/dist/plugins.js +239 -0
  118. package/dist/project-discovery.d.ts +3 -0
  119. package/dist/project-discovery.js +57 -0
  120. package/dist/project-files-api.d.ts +16 -0
  121. package/dist/project-files-api.js +243 -0
  122. package/dist/project-files.d.ts +97 -0
  123. package/dist/project-files.js +557 -0
  124. package/dist/project-lock.d.ts +4 -0
  125. package/dist/project-lock.js +35 -0
  126. package/dist/project-probe-worker.d.ts +1 -0
  127. package/dist/project-probe-worker.js +17 -0
  128. package/dist/project-probe.d.ts +22 -0
  129. package/dist/project-probe.js +67 -0
  130. package/dist/projects.d.ts +29 -0
  131. package/dist/projects.js +219 -0
  132. package/dist/publication.d.ts +31 -0
  133. package/dist/publication.js +642 -0
  134. package/dist/resource-api.d.ts +15 -0
  135. package/dist/resource-api.js +256 -0
  136. package/dist/resources-orphans.d.ts +41 -0
  137. package/dist/resources-orphans.js +235 -0
  138. package/dist/resources.d.ts +246 -0
  139. package/dist/resources.js +714 -0
  140. package/dist/run-api.d.ts +10 -0
  141. package/dist/run-api.js +308 -0
  142. package/dist/run-context.d.ts +54 -0
  143. package/dist/run-context.js +137 -0
  144. package/dist/runs.d.ts +82 -0
  145. package/dist/runs.js +506 -0
  146. package/dist/runtime-backend.d.ts +24 -0
  147. package/dist/runtime-backend.js +214 -0
  148. package/dist/runtime.d.ts +97 -0
  149. package/dist/runtime.js +553 -0
  150. package/dist/scheduler.d.ts +85 -0
  151. package/dist/scheduler.js +344 -0
  152. package/dist/service.d.ts +29 -0
  153. package/dist/service.js +515 -0
  154. package/dist/skills-api.d.ts +18 -0
  155. package/dist/skills-api.js +120 -0
  156. package/dist/storage-api.d.ts +21 -0
  157. package/dist/storage-api.js +92 -0
  158. package/dist/transfer-api.d.ts +10 -0
  159. package/dist/transfer-api.js +60 -0
  160. package/dist/vendor/protocol/contracts.typecheck.d.ts +1 -0
  161. package/dist/vendor/protocol/contracts.typecheck.js +35 -0
  162. package/dist/vendor/protocol/execution-chain.d.ts +6 -0
  163. package/dist/vendor/protocol/execution-chain.js +26 -0
  164. package/dist/vendor/protocol/graph-binary.d.ts +15 -0
  165. package/dist/vendor/protocol/graph-binary.js +121 -0
  166. package/dist/vendor/protocol/graph-title.d.ts +2 -0
  167. package/dist/vendor/protocol/graph-title.js +23 -0
  168. package/dist/vendor/protocol/index.d.ts +617 -0
  169. package/dist/vendor/protocol/index.js +20 -0
  170. package/dist/vendor/protocol/pairing.d.ts +29 -0
  171. package/dist/vendor/protocol/pairing.js +71 -0
  172. package/dist/vendor/protocol/preview.d.ts +2 -0
  173. package/dist/vendor/protocol/preview.js +9 -0
  174. package/dist/vendor/protocol/resource-mime.d.ts +2 -0
  175. package/dist/vendor/protocol/resource-mime.js +35 -0
  176. package/dist/video-thumbnail.d.ts +2 -0
  177. package/dist/video-thumbnail.js +34 -0
  178. package/package.json +9 -0
@@ -0,0 +1,35 @@
1
+ import { createServer } from 'node:net';
2
+ import { createHash } from 'node:crypto';
3
+ import { userInfo } from 'node:os';
4
+ import { isAbsolute, resolve } from 'node:path';
5
+ import { ServiceError } from './errors.js';
6
+ export function projectLockPort(canonicalPath) {
7
+ if (!isAbsolute(canonicalPath) || resolve(canonicalPath) !== canonicalPath || canonicalPath.includes('\0'))
8
+ throw new ServiceError('INVALID_REQUEST', 'Project lock requires the canonical absolute path');
9
+ const user = userInfo();
10
+ const hash = createHash('sha256').update(JSON.stringify(['openworkgraph-project-v1', user.uid, user.username, canonicalPath])).digest();
11
+ return 20000 + hash.readUInt32BE(0) % 40000;
12
+ }
13
+ /** Kernel lifetime lock shared by service roots. Port collision is contention,
14
+ * never evidence that a PID can be killed. Does not write into the source tree. */
15
+ export async function acquireProjectLock(canonicalPath) {
16
+ const port = projectLockPort(canonicalPath);
17
+ const server = createServer(socket => { socket.on('error', () => { }); socket.destroy(); });
18
+ const acquired = await new Promise((resolve, reject) => {
19
+ const error = (cause) => {
20
+ if (cause.code === 'EADDRINUSE')
21
+ resolve(false);
22
+ else
23
+ reject(new ServiceError('PROJECT_UNAVAILABLE', 'Cannot acquire project lifetime lock', { cause }));
24
+ };
25
+ server.once('error', error);
26
+ server.listen({ host: '127.0.0.1', port, exclusive: true }, () => { server.off('error', error); resolve(true); });
27
+ });
28
+ if (!acquired)
29
+ return undefined;
30
+ // An unresolved writer keeps the process/lock alive even if the HTTP UI closes.
31
+ let released = false;
32
+ return () => { if (released)
33
+ return; released = true; server.close(); };
34
+ }
35
+ //# sourceMappingURL=project-lock.js.map
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,17 @@
1
+ import { parentPort, workerData } from 'node:worker_threads';
2
+ import { discoverProjects } from './project-discovery.js';
3
+ import { inspectProjectDirectory } from './projects.js';
4
+ import { ServiceError } from './errors.js';
5
+ const job = workerData;
6
+ if (job.kind === 'discover')
7
+ parentPort.postMessage(discoverProjects(job.codexHome));
8
+ else
9
+ parentPort.postMessage((job.paths ?? []).map(path => {
10
+ try {
11
+ return { path, value: inspectProjectDirectory(path) };
12
+ }
13
+ catch (error) {
14
+ return { path, error: error instanceof ServiceError ? error.code : 'PROJECT_UNAVAILABLE' };
15
+ }
16
+ }));
17
+ //# sourceMappingURL=project-probe-worker.js.map
@@ -0,0 +1,22 @@
1
+ import type { ProjectCandidates } from "./vendor/protocol/index.js";
2
+ type Inspection = {
3
+ canonicalPath: string;
4
+ isGit: boolean;
5
+ };
6
+ /** Filesystem calls run outside the event loop and SQLite transactions.
7
+ * Timed-out workers retain their capacity slot until actual exit (bounded stuck mounts). */
8
+ export declare class ProjectProbe {
9
+ private readonly options;
10
+ private readonly workers;
11
+ private closed;
12
+ constructor(options?: {
13
+ timeoutMs?: number;
14
+ maxWorkers?: number;
15
+ workerUrl?: URL;
16
+ });
17
+ private run;
18
+ inspect(paths: string[]): Promise<(path: string) => Inspection>;
19
+ discover(codexHome?: string): Promise<ProjectCandidates>;
20
+ close(): void;
21
+ }
22
+ export {};
@@ -0,0 +1,67 @@
1
+ import { Worker } from 'node:worker_threads';
2
+ import { ServiceError } from './errors.js';
3
+ /** Filesystem calls run outside the event loop and SQLite transactions.
4
+ * Timed-out workers retain their capacity slot until actual exit (bounded stuck mounts). */
5
+ export class ProjectProbe {
6
+ options;
7
+ workers = new Map();
8
+ closed = false;
9
+ constructor(options = {}) {
10
+ this.options = options;
11
+ }
12
+ run(job) {
13
+ if (this.closed || this.workers.size >= (this.options.maxWorkers ?? 4))
14
+ return Promise.reject(new ServiceError('PROJECT_UNAVAILABLE', '目录探测繁忙,请稍后重试。'));
15
+ return new Promise((resolve, reject) => {
16
+ const worker = new Worker(this.options.workerUrl ?? new URL('./project-probe-worker.js', import.meta.url), { workerData: job });
17
+ let settled = false;
18
+ const finish = (error, value) => {
19
+ if (settled)
20
+ return;
21
+ settled = true;
22
+ clearTimeout(timer);
23
+ if (error)
24
+ reject(error);
25
+ else
26
+ resolve(value);
27
+ };
28
+ const timer = setTimeout(() => {
29
+ finish(new ServiceError('PROJECT_UNAVAILABLE', '目录探测超时,未修改项目;请检查挂载设备后重试。'));
30
+ void worker.terminate();
31
+ }, this.options.timeoutMs ?? 5000);
32
+ this.workers.set(worker, () => finish(new ServiceError('PROJECT_UNAVAILABLE', '服务停止,目录探测已取消。')));
33
+ worker.on('message', (value) => finish(null, value));
34
+ worker.on('error', () => finish(new ServiceError('PROJECT_UNAVAILABLE', '目录探测失败;请检查本机目录。')));
35
+ worker.on('exit', () => { this.workers.delete(worker); finish(new ServiceError('PROJECT_UNAVAILABLE', '目录探测未完成。')); });
36
+ worker.unref();
37
+ });
38
+ }
39
+ async inspect(paths) {
40
+ const results = await this.run({ kind: 'inspect', paths: [...new Set(paths)] });
41
+ const byPath = new Map(results.map(result => [result.path, result]));
42
+ for (const result of results)
43
+ if (result.value && !byPath.has(result.value.canonicalPath))
44
+ byPath.set(result.value.canonicalPath, result);
45
+ return path => {
46
+ const result = byPath.get(path);
47
+ if (!result)
48
+ throw new ServiceError('CONFLICT', '项目路径在探测期间变化;请刷新后重试。');
49
+ if (!result.value)
50
+ throw new ServiceError(result.error ?? 'PROJECT_UNAVAILABLE', '目录不可用;请检查路径。');
51
+ return result.value;
52
+ };
53
+ }
54
+ async discover(codexHome) {
55
+ try {
56
+ return await this.run({ kind: 'discover', codexHome });
57
+ }
58
+ catch {
59
+ return { source: 'codex-desktop-saved-roots', status: 'unavailable', reason: '目录候选探测超时、繁忙或不可用;可手动注册路径。', candidates: [] };
60
+ }
61
+ }
62
+ close() { this.closed = true; for (const [worker, cancel] of this.workers) {
63
+ cancel();
64
+ void worker.terminate();
65
+ } }
66
+ }
67
+ //# sourceMappingURL=project-probe.js.map
@@ -0,0 +1,29 @@
1
+ import type { DatabaseSync } from 'node:sqlite';
2
+ import type { Project } from "./vendor/protocol/index.js";
3
+ export declare function inspectProjectDirectory(path: string): {
4
+ canonicalPath: string;
5
+ isGit: boolean;
6
+ };
7
+ export declare class Projects {
8
+ readonly db: DatabaseSync;
9
+ readonly serviceId: string;
10
+ readonly inspectDirectory: typeof inspectProjectDirectory;
11
+ constructor(db: DatabaseSync, serviceId: string, inspectDirectory?: typeof inspectProjectDirectory);
12
+ /** Join auth.withSession; a failed operation remains atomic even if its caller catches it. */
13
+ private atomic;
14
+ private row;
15
+ private project;
16
+ list(state?: Project['state'] | 'all'): Project[];
17
+ get(id: string): Project;
18
+ private unique;
19
+ private noUnfinishedRuns;
20
+ private changed;
21
+ register(path: string): Project;
22
+ setState(id: string, state: Project['state']): Project;
23
+ repairPath(id: string, path: string): Project;
24
+ /** Guard metadata writes; source availability is required separately for execution. */
25
+ assertWritable(id: string): Project;
26
+ /** Future run submission must call this within its own acceptance transaction.
27
+ * Returning a path does not lock the filesystem against subsequent external moves. */
28
+ resolveCwd(id: string): string;
29
+ }
@@ -0,0 +1,219 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { accessSync, closeSync, constants, fstatSync, openSync, readSync, realpathSync, statSync } from 'node:fs';
3
+ import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
4
+ import { TERMINAL_RUN_STATUSES } from "./vendor/protocol/index.js";
5
+ import { ServiceError } from './errors.js';
6
+ import { transaction } from './persistence/database.js';
7
+ import { Repositories } from './persistence/repositories.js';
8
+ function canonicalDirectory(path) {
9
+ if (typeof path !== 'string' || path.length > 32768 || !isAbsolute(path) || path.includes('\0'))
10
+ throw new ServiceError('INVALID_REQUEST', '项目路径必须是绝对目录路径。');
11
+ try {
12
+ const canonical = realpathSync.native(path);
13
+ if (!statSync(canonical).isDirectory())
14
+ throw new Error('Not a directory');
15
+ accessSync(canonical, constants.R_OK | constants.X_OK);
16
+ return canonical;
17
+ }
18
+ catch (cause) {
19
+ throw new ServiceError('PROJECT_UNAVAILABLE', '项目目录不存在或不可访问;请修复路径,不会自动创建目录。', { cause });
20
+ }
21
+ }
22
+ function readGitFile(path) {
23
+ const fd = openSync(path, constants.O_RDONLY | constants.O_NONBLOCK | (constants.O_NOFOLLOW ?? 0));
24
+ try {
25
+ const info = fstatSync(fd);
26
+ if (!info.isFile() || info.size > 4096)
27
+ throw new Error('Invalid Git metadata file');
28
+ const buffer = Buffer.alloc(4097);
29
+ let length = 0;
30
+ while (length < buffer.length) {
31
+ const count = readSync(fd, buffer, length, buffer.length - length, null);
32
+ if (!count)
33
+ break;
34
+ length += count;
35
+ }
36
+ if (length > 4096)
37
+ throw new Error('Git metadata file too large');
38
+ return buffer.subarray(0, length).toString('utf8');
39
+ }
40
+ finally {
41
+ closeSync(fd);
42
+ }
43
+ }
44
+ /** Inspect Git metadata only: no processes, hooks, config execution or filesystem writes. */
45
+ function isGitDirectory(path) {
46
+ for (let current = path;; current = dirname(current)) {
47
+ try {
48
+ const marker = join(current, '.git');
49
+ const info = statSync(marker);
50
+ let gitDir = marker;
51
+ if (info.isFile()) {
52
+ if (info.size > 4096)
53
+ return false;
54
+ const match = /^gitdir: (.+)\r?\n?$/.exec(readGitFile(marker));
55
+ if (!match)
56
+ return false;
57
+ gitDir = resolve(current, match[1].trim());
58
+ }
59
+ else if (!info.isDirectory())
60
+ return false;
61
+ let commonDir = gitDir;
62
+ try {
63
+ const common = join(gitDir, 'commondir');
64
+ const value = readGitFile(common).trim();
65
+ if (!value)
66
+ return false;
67
+ commonDir = resolve(gitDir, value);
68
+ }
69
+ catch (error) {
70
+ if (error.code !== 'ENOENT')
71
+ return false;
72
+ }
73
+ return statSync(join(gitDir, 'HEAD')).isFile() && statSync(join(commonDir, 'objects')).isDirectory() && statSync(join(commonDir, 'refs')).isDirectory();
74
+ }
75
+ catch (error) {
76
+ if (error.code !== 'ENOENT')
77
+ return false;
78
+ }
79
+ if (dirname(current) === current)
80
+ return false;
81
+ }
82
+ }
83
+ export function inspectProjectDirectory(path) {
84
+ const canonicalPath = canonicalDirectory(path);
85
+ return { canonicalPath, isGit: isGitDirectory(canonicalPath) };
86
+ }
87
+ export class Projects {
88
+ db;
89
+ serviceId;
90
+ inspectDirectory;
91
+ constructor(db, serviceId, inspectDirectory = inspectProjectDirectory) {
92
+ this.db = db;
93
+ this.serviceId = serviceId;
94
+ this.inspectDirectory = inspectDirectory;
95
+ }
96
+ /** Join auth.withSession; a failed operation remains atomic even if its caller catches it. */
97
+ atomic(work) {
98
+ if (!this.db.isTransaction)
99
+ return transaction(this.db, work);
100
+ const savepoint = 'projects_' + randomUUID().replaceAll('-', '');
101
+ this.db.exec(`SAVEPOINT ${savepoint}`);
102
+ try {
103
+ const result = work();
104
+ this.db.exec(`RELEASE ${savepoint}`);
105
+ return result;
106
+ }
107
+ catch (error) {
108
+ this.db.exec(`ROLLBACK TO ${savepoint}; RELEASE ${savepoint}`);
109
+ throw error;
110
+ }
111
+ }
112
+ row(id) {
113
+ if (typeof id !== 'string' || !id)
114
+ throw new ServiceError('INVALID_REQUEST', '需要 projectId。');
115
+ const row = this.db.prepare('SELECT * FROM projects WHERE id=?').get(id);
116
+ if (!row)
117
+ throw new ServiceError('NOT_FOUND', '项目不存在。');
118
+ return row;
119
+ }
120
+ project(row) {
121
+ let availability = 'unavailable';
122
+ try {
123
+ if (this.inspectDirectory(row.canonical_path).canonicalPath === row.canonical_path)
124
+ availability = 'available';
125
+ }
126
+ catch (error) {
127
+ if (error instanceof ServiceError && error.code === 'CONFLICT')
128
+ throw error;
129
+ if (!(error instanceof ServiceError) || error.code !== 'PROJECT_UNAVAILABLE')
130
+ availability = 'unknown';
131
+ }
132
+ return { serviceId: this.serviceId, projectId: row.id, name: row.name, canonicalPath: row.canonical_path, state: row.state, availability, isGit: row.is_git === 1 };
133
+ }
134
+ list(state = 'active') {
135
+ if (!['active', 'inactive', 'all'].includes(state))
136
+ throw new ServiceError('INVALID_REQUEST', '无效的项目状态筛选。');
137
+ return this.atomic(() => {
138
+ const rows = this.db.prepare("SELECT * FROM projects WHERE ?='all' OR state=? ORDER BY name,id").all(state, state);
139
+ return rows.map(row => this.project(row));
140
+ });
141
+ }
142
+ get(id) { return this.atomic(() => this.project(this.row(id))); }
143
+ unique(path, exceptId = null) {
144
+ const row = this.db.prepare('SELECT id FROM projects WHERE canonical_path=?').get(path);
145
+ if (row && row['id'] !== exceptId)
146
+ throw new ServiceError('CONFLICT', '此目录已注册为另一个项目;请使用已有项目。');
147
+ }
148
+ noUnfinishedRuns(id) {
149
+ const placeholders = TERMINAL_RUN_STATUSES.map(() => '?').join(',');
150
+ if (this.db.prepare(`SELECT 1 FROM runs WHERE project_id=? AND status NOT IN (${placeholders}) LIMIT 1`).get(id, ...TERMINAL_RUN_STATUSES)) {
151
+ throw new ServiceError('ACTIVE_RUN', '项目有未结束的执行或生成任务,不能停用或修改路径。');
152
+ }
153
+ }
154
+ changed(row) {
155
+ const project = this.project(row);
156
+ const revision = Number(this.db.prepare("SELECT COALESCE(MAX(revision),0)+1 AS revision FROM events WHERE type='project.changed' AND entity_id=?").get(row.id)['revision']);
157
+ new Repositories(this.db).appendEvent({ eventId: randomUUID(), type: 'project.changed', projectId: row.id, graphId: null, entityId: row.id, revision, occurredAt: new Date().toISOString(), payload: { ...project } });
158
+ return project;
159
+ }
160
+ register(path) {
161
+ return this.atomic(() => {
162
+ const inspected = this.inspectDirectory(path);
163
+ const canonical = inspected.canonicalPath;
164
+ this.unique(canonical);
165
+ const row = { id: randomUUID(), canonical_path: canonical, name: basename(canonical) || canonical, state: 'active', is_git: Number(inspected.isGit) };
166
+ this.db.prepare('INSERT INTO projects(id,canonical_path,name,state,is_git) VALUES(?,?,?,?,?)').run(row.id, row.canonical_path, row.name, row.state, row.is_git);
167
+ return this.changed(row);
168
+ });
169
+ }
170
+ setState(id, state) {
171
+ if (state !== 'active' && state !== 'inactive')
172
+ throw new ServiceError('INVALID_REQUEST', '项目状态必须是 active 或 inactive。');
173
+ return this.atomic(() => {
174
+ const row = this.row(id);
175
+ if (row.state === state)
176
+ return this.project(row);
177
+ if (state === 'inactive')
178
+ this.noUnfinishedRuns(id);
179
+ this.db.prepare('UPDATE projects SET state=? WHERE id=?').run(state, id);
180
+ return this.changed({ ...row, state });
181
+ });
182
+ }
183
+ repairPath(id, path) {
184
+ return this.atomic(() => {
185
+ const row = this.row(id);
186
+ if (row.state !== 'active')
187
+ throw new ServiceError('PROJECT_INACTIVE', '停用项目只读;请先启用再修复路径。');
188
+ this.noUnfinishedRuns(id);
189
+ const inspected = this.inspectDirectory(path);
190
+ const canonical = inspected.canonicalPath;
191
+ this.unique(canonical, id);
192
+ const isGit = Number(inspected.isGit);
193
+ if (canonical === row.canonical_path && isGit === row.is_git)
194
+ return this.project(row);
195
+ this.db.prepare('UPDATE projects SET canonical_path=?,is_git=? WHERE id=?').run(canonical, isGit, id);
196
+ return this.changed({ ...row, canonical_path: canonical, is_git: isGit });
197
+ });
198
+ }
199
+ /** Guard metadata writes; source availability is required separately for execution. */
200
+ assertWritable(id) {
201
+ return this.atomic(() => {
202
+ const project = this.project(this.row(id));
203
+ if (project.state !== 'active')
204
+ throw new ServiceError('PROJECT_INACTIVE', '停用项目只读,不能提交新任务。');
205
+ return project;
206
+ });
207
+ }
208
+ /** Future run submission must call this within its own acceptance transaction.
209
+ * Returning a path does not lock the filesystem against subsequent external moves. */
210
+ resolveCwd(id) {
211
+ return this.atomic(() => {
212
+ const project = this.assertWritable(id);
213
+ if (project.availability !== 'available')
214
+ throw new ServiceError('PROJECT_UNAVAILABLE', '项目目录不可用或符号链接目标已变化;请修复项目路径。');
215
+ return project.canonicalPath;
216
+ });
217
+ }
218
+ }
219
+ //# sourceMappingURL=projects.js.map
@@ -0,0 +1,31 @@
1
+ import type { DatabaseSync } from 'node:sqlite';
2
+ import type { GraphScope, Run } from "./vendor/protocol/index.js";
3
+ import { type DataDirectories } from './directories.js';
4
+ import { Graphs } from './graphs.js';
5
+ import { Resources } from './resources.js';
6
+ import { Runs, type RunToken } from './runs.js';
7
+ export interface CandidateDecision extends GraphScope {
8
+ runId: string;
9
+ expectedContentVersion: number;
10
+ decision: 'accept' | 'discard';
11
+ }
12
+ /** Validate API image bytes with the same structural checks used at publication. */
13
+ export declare function validateImageOutput(bytes: Buffer): 'image/png' | 'image/jpeg' | 'image/webp';
14
+ /** File preparation never runs inside a transaction. Only this publisher may
15
+ * interpret bridge result.answer; natural-language completion is not parsed. */
16
+ export declare class Publication {
17
+ readonly db: DatabaseSync;
18
+ readonly dirs: DataDirectories;
19
+ readonly resources: Resources;
20
+ readonly graphs: Graphs;
21
+ readonly runs: Runs;
22
+ constructor(db: DatabaseSync, dirs: DataDirectories, resources: Resources, graphs: Graphs, runs: Runs);
23
+ private check;
24
+ publish(run: Run, token: RunToken): Promise<void>;
25
+ private append;
26
+ /** Main's authenticated route must supply full scope. No implicit acceptance. */
27
+ decide(request: CandidateDecision): {
28
+ state: 'accepted' | 'discarded';
29
+ contentVersion: number;
30
+ };
31
+ }