@nurix/nustack 0.10.0-dev.13 → 0.10.0-dev.14
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/dist/commands/bootstrap.js +4 -4
- package/dist/commands/docs.js +13 -13
- package/dist/commands/doctor.js +13 -13
- package/dist/commands/handoff.js +8 -8
- package/dist/commands/init.js +43 -43
- package/dist/commands/{workspace-sync.js → project-sync.js} +5 -5
- package/dist/commands/{workspace.js → project.js} +133 -133
- package/dist/commands/review.js +5 -5
- package/dist/commands/search.js +7 -7
- package/dist/commands/sessions.js +1 -1
- package/dist/commands/sync.js +10 -10
- package/dist/commands/toolkit.js +19 -19
- package/dist/index.js +29 -29
- package/dist/lib/etna/profile.js +9 -9
- package/dist/lib/etna/reconcile.js +11 -11
- package/dist/lib/git-sync/core/engine.js +4 -4
- package/dist/lib/git-sync/inbound/apply_commit.js +3 -3
- package/dist/lib/git-sync/inbound/approval.js +1 -1
- package/dist/lib/git-sync/inbound/download.js +8 -8
- package/dist/lib/git-sync/inbound/event_sync.js +13 -13
- package/dist/lib/git-sync/inbound/notification.js +1 -1
- package/dist/lib/git-sync/outbound/extraction.js +4 -4
- package/dist/lib/git-sync/outbound/history_decision.js +2 -2
- package/dist/lib/git-sync/outbound/publisher.js +2 -2
- package/dist/lib/git-sync/outbound/queue.js +3 -3
- package/dist/lib/git-sync/outbound/upload_client.js +4 -4
- package/dist/lib/git-sync/whitelist/index.js +1 -1
- package/dist/lib/git-sync/whitelist/types.js +1 -1
- package/dist/lib/git-sync/whitelist/validation.js +5 -5
- package/dist/lib/lanes/supervisor.js +8 -8
- package/dist/lib/local_store/accepted_uploads.js +5 -5
- package/dist/lib/local_store/bindings.js +11 -11
- package/dist/lib/local_store/index.js +1 -1
- package/dist/lib/local_store/migrations.js +13 -0
- package/dist/lib/local_store/project_events_cache.js +54 -0
- package/dist/lib/nustack_client.js +45 -45
- package/dist/lib/onboarding/attach_existing_git.js +7 -7
- package/dist/lib/onboarding/binding.js +11 -11
- package/dist/lib/onboarding/folder_state.js +10 -10
- package/dist/lib/onboarding/handoff_apply.js +2 -2
- package/dist/lib/onboarding/init_fresh_folder.js +18 -18
- package/dist/lib/onboarding/manifest_writer.js +31 -31
- package/dist/lib/onboarding/web_first_attach.js +12 -12
- package/dist/lib/{workspace_context.js → project_context.js} +24 -24
- package/dist/lib/review/client.js +1 -1
- package/dist/lib/session_telemetry.js +2 -2
- package/dist/lib/sessions/client.js +8 -8
- package/dist/lib/sessions/outsideSessions.js +3 -3
- package/dist/lib/telemetry/config_renderer.js +3 -3
- package/dist/lib/telemetry/health_report.js +1 -1
- package/package.json +3 -3
- package/dist/lib/local_store/workspace_events_cache.js +0 -54
|
@@ -49,11 +49,11 @@ export async function publishCurrentState(store, binding, repo, deps) {
|
|
|
49
49
|
repo,
|
|
50
50
|
client: deps.client,
|
|
51
51
|
whitelistRef: { current: whitelist },
|
|
52
|
-
|
|
52
|
+
projectId: binding.projectId,
|
|
53
53
|
now: deps.now ?? Date.now,
|
|
54
54
|
};
|
|
55
55
|
for (let staleDepth = 0;; staleDepth++) {
|
|
56
|
-
const delta = computeTreeDelta(repo, base, head, ctx.whitelistRef.current, binding.
|
|
56
|
+
const delta = computeTreeDelta(repo, base, head, ctx.whitelistRef.current, binding.projectId);
|
|
57
57
|
if (!delta.ok)
|
|
58
58
|
return { ok: false, code: delta.code, detail: delta.path };
|
|
59
59
|
const result = await pushOperations(ctx, head, base, delta.delta.operations);
|
|
@@ -19,7 +19,7 @@ export async function runOutboundPublisher(ctx, runtime) {
|
|
|
19
19
|
repo,
|
|
20
20
|
client: runtime.client,
|
|
21
21
|
whitelistRef: { current: whitelist },
|
|
22
|
-
|
|
22
|
+
projectId: binding.projectId,
|
|
23
23
|
now,
|
|
24
24
|
};
|
|
25
25
|
for (const commitId of commitIds) {
|
|
@@ -32,7 +32,7 @@ export async function runOutboundPublisher(ctx, runtime) {
|
|
|
32
32
|
switch (outcome.kind) {
|
|
33
33
|
case "accepted":
|
|
34
34
|
updateBindingCursor(store, binding.id, { lastProcessedCommitId: commitId });
|
|
35
|
-
emit({ event: "upload_result", bindingId: binding.id, commitId, outcome: "accepted", handoffId: outcome.handoffId,
|
|
35
|
+
emit({ event: "upload_result", bindingId: binding.id, commitId, outcome: "accepted", handoffId: outcome.handoffId, projectSequence: outcome.projectSequence });
|
|
36
36
|
break;
|
|
37
37
|
case "no_op":
|
|
38
38
|
updateBindingCursor(store, binding.id, { lastProcessedCommitId: commitId });
|
|
@@ -24,7 +24,7 @@ export async function submitHead(ctx, commitId) {
|
|
|
24
24
|
return attempt(ctx, commitId, 0);
|
|
25
25
|
}
|
|
26
26
|
async function attempt(ctx, commitId, staleDepth) {
|
|
27
|
-
const extraction = extractCommitDelta(ctx.repo, commitId, ctx.whitelistRef.current, ctx.
|
|
27
|
+
const extraction = extractCommitDelta(ctx.repo, commitId, ctx.whitelistRef.current, ctx.projectId);
|
|
28
28
|
if (!extraction.ok) {
|
|
29
29
|
return blockBinding(ctx, commitId, extraction.code, { path: extraction.path });
|
|
30
30
|
}
|
|
@@ -85,11 +85,11 @@ export async function pushOperations(ctx, commitId, parentCommitId, operations)
|
|
|
85
85
|
bindingId: ctx.binding.id,
|
|
86
86
|
commitId,
|
|
87
87
|
handoffId: outcome.accepted.handoffId,
|
|
88
|
-
|
|
88
|
+
projectSequence: outcome.accepted.projectSequence,
|
|
89
89
|
manifestSha256: built.manifestSha256,
|
|
90
90
|
});
|
|
91
91
|
deleteUpload(ctx.store, ctx.binding.id, commitId);
|
|
92
|
-
return { kind: "accepted", handoffId: outcome.accepted.handoffId,
|
|
92
|
+
return { kind: "accepted", handoffId: outcome.accepted.handoffId, projectSequence: outcome.accepted.projectSequence };
|
|
93
93
|
}
|
|
94
94
|
async function repinWhitelist(ctx) {
|
|
95
95
|
try {
|
|
@@ -18,7 +18,7 @@ const TERMINAL_CODES = new Set([
|
|
|
18
18
|
"INVALID_STATE",
|
|
19
19
|
"IDEMPOTENCY_CONFLICT",
|
|
20
20
|
"HANDOFF_SUPERSEDED",
|
|
21
|
-
"
|
|
21
|
+
"PROJECT_NOT_FOUND",
|
|
22
22
|
"DOCUMENT_NOT_FOUND",
|
|
23
23
|
]);
|
|
24
24
|
const AUTH_CODES = new Set(["AUTH_REQUIRED", "ACCESS_TOKEN_EXPIRED", "DEVICE_CREDENTIAL_INVALID"]);
|
|
@@ -120,11 +120,11 @@ export function createUploadClient(deps) {
|
|
|
120
120
|
const data = (response.body?.data ?? {});
|
|
121
121
|
if (data.noOp)
|
|
122
122
|
return { ok: true, value: { noOp: true } };
|
|
123
|
-
const
|
|
124
|
-
if (!data.handoffId || typeof
|
|
123
|
+
const projectSequence = data.projectSequence ?? response.body?.meta?.projectSequence;
|
|
124
|
+
if (!data.handoffId || typeof projectSequence !== "number") {
|
|
125
125
|
return { ok: false, error: { kind: "terminal", code: "MALFORMED_ACCEPTANCE" } };
|
|
126
126
|
}
|
|
127
|
-
return { ok: true, value: { accepted: { handoffId: data.handoffId,
|
|
127
|
+
return { ok: true, value: { accepted: { handoffId: data.handoffId, projectSequence } } };
|
|
128
128
|
});
|
|
129
129
|
},
|
|
130
130
|
reportHealth(bindingId, report) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { SYNC_PROTOCOL_VERSION, isWhitelistManifest, } from "./types.js";
|
|
2
2
|
export { matchPath, isProtectedPath, globMatch } from "./matcher.js";
|
|
3
|
-
export { validateOperationContent, mediaTypeForPath,
|
|
3
|
+
export { validateOperationContent, mediaTypeForPath, frontmatterProjectId, } from "./validation.js";
|
|
4
4
|
export { fetchWhitelist, pinWhitelist, loadPinnedWhitelist, canonicalWhitelistSha256, whitelistCacheFile, WhitelistError, } from "./store.js";
|
|
@@ -36,7 +36,7 @@ export function isWhitelistManifest(value) {
|
|
|
36
36
|
return (manifest.schemaVersion === 1 &&
|
|
37
37
|
typeof manifest.manifestId === "string" &&
|
|
38
38
|
typeof manifest.version === "number" &&
|
|
39
|
-
typeof manifest.
|
|
39
|
+
typeof manifest.projectId === "string" &&
|
|
40
40
|
typeof manifest.protocolVersion === "string" &&
|
|
41
41
|
Array.isArray(manifest.entries) &&
|
|
42
42
|
manifest.entries.every(isEntry) &&
|
|
@@ -14,7 +14,7 @@ export function mediaTypeForPath(path) {
|
|
|
14
14
|
const ext = path.slice(dot + 1).toLowerCase();
|
|
15
15
|
return MEDIA_TYPE_BY_EXTENSION[ext] ?? null;
|
|
16
16
|
}
|
|
17
|
-
export function
|
|
17
|
+
export function frontmatterProjectId(text) {
|
|
18
18
|
if (!text.startsWith("---"))
|
|
19
19
|
return null;
|
|
20
20
|
const firstNewline = text.indexOf("\n");
|
|
@@ -26,7 +26,7 @@ export function frontmatterWorkspaceId(text) {
|
|
|
26
26
|
const closeIndex = rest.search(/\n---\s*(\n|$)/);
|
|
27
27
|
const block = closeIndex < 0 ? rest : rest.slice(0, closeIndex);
|
|
28
28
|
for (const line of block.split("\n")) {
|
|
29
|
-
const match = /^\s*(?:
|
|
29
|
+
const match = /^\s*(?:project_id|projectId|app_id|appId)\s*:\s*(.+?)\s*$/.exec(line);
|
|
30
30
|
if (match)
|
|
31
31
|
return match[1].replace(/^["']|["']$/g, "");
|
|
32
32
|
}
|
|
@@ -52,9 +52,9 @@ export function validateOperationContent(input) {
|
|
|
52
52
|
}
|
|
53
53
|
if (text.includes(NUL))
|
|
54
54
|
return { ok: false, code: "invalid_encoding" };
|
|
55
|
-
const
|
|
56
|
-
if (
|
|
57
|
-
return { ok: false, code: "
|
|
55
|
+
const declaredProject = frontmatterProjectId(text);
|
|
56
|
+
if (declaredProject !== null && declaredProject !== input.projectId) {
|
|
57
|
+
return { ok: false, code: "cross_project_marker" };
|
|
58
58
|
}
|
|
59
59
|
return { ok: true };
|
|
60
60
|
}
|
|
@@ -40,7 +40,7 @@ export class LaneSupervisor {
|
|
|
40
40
|
registryPath() {
|
|
41
41
|
return path.join(lanesHome(this.env), "registry.json");
|
|
42
42
|
}
|
|
43
|
-
async
|
|
43
|
+
async boundProjectOf(projectPath) {
|
|
44
44
|
try {
|
|
45
45
|
const parsed = JSON.parse(await readFile(path.join(projectPath, ".nustack", "workspace.json"), "utf8"));
|
|
46
46
|
const id = parsed?.workspaceId;
|
|
@@ -52,10 +52,10 @@ export class LaneSupervisor {
|
|
|
52
52
|
}
|
|
53
53
|
async moveLaneSession(record, to) {
|
|
54
54
|
const client = this.options.sessions;
|
|
55
|
-
if (!client || !record.
|
|
55
|
+
if (!client || !record.projectId)
|
|
56
56
|
return;
|
|
57
57
|
try {
|
|
58
|
-
await client.transitionSession(record.
|
|
58
|
+
await client.transitionSession(record.projectId, record.laneId, to);
|
|
59
59
|
}
|
|
60
60
|
catch {
|
|
61
61
|
}
|
|
@@ -153,7 +153,7 @@ export class LaneSupervisor {
|
|
|
153
153
|
providerSessionId: null,
|
|
154
154
|
createdAt: new Date().toISOString(),
|
|
155
155
|
status: "open",
|
|
156
|
-
|
|
156
|
+
projectId: await this.boundProjectOf(projectPath),
|
|
157
157
|
};
|
|
158
158
|
const adapterOptions = {
|
|
159
159
|
cwd: worktree.worktreePath,
|
|
@@ -183,8 +183,8 @@ export class LaneSupervisor {
|
|
|
183
183
|
record.providerSessionId = sessionId;
|
|
184
184
|
void this.updateRegistry((lanes) => lanes.map((row) => (row.laneId === laneId ? { ...row, providerSessionId: sessionId } : row)));
|
|
185
185
|
const client = this.options.sessions;
|
|
186
|
-
if (client && record.
|
|
187
|
-
void client.setVendorSessionId(record.
|
|
186
|
+
if (client && record.projectId) {
|
|
187
|
+
void client.setVendorSessionId(record.projectId, laneId, sessionId).catch(() => undefined);
|
|
188
188
|
}
|
|
189
189
|
},
|
|
190
190
|
onExit: (code) => {
|
|
@@ -207,9 +207,9 @@ export class LaneSupervisor {
|
|
|
207
207
|
}
|
|
208
208
|
this.lanes.set(laneId, { adapter, record });
|
|
209
209
|
await this.updateRegistry((lanes) => [...lanes.filter((row) => row.laneId !== laneId), record]);
|
|
210
|
-
if (this.options.sessions && record.
|
|
210
|
+
if (this.options.sessions && record.projectId) {
|
|
211
211
|
try {
|
|
212
|
-
await this.options.sessions.createSession(record.
|
|
212
|
+
await this.options.sessions.createSession(record.projectId, {
|
|
213
213
|
id: laneId,
|
|
214
214
|
startedBy: "person",
|
|
215
215
|
substrate: "device",
|
|
@@ -3,7 +3,7 @@ function mapRow(row) {
|
|
|
3
3
|
bindingId: row.binding_id,
|
|
4
4
|
commitId: row.commit_id,
|
|
5
5
|
handoffId: row.handoff_id,
|
|
6
|
-
|
|
6
|
+
projectSequence: row.project_sequence,
|
|
7
7
|
manifestSha256: row.manifest_sha256,
|
|
8
8
|
createdAtMs: row.created_at_ms,
|
|
9
9
|
};
|
|
@@ -11,16 +11,16 @@ function mapRow(row) {
|
|
|
11
11
|
export function recordAcceptedUpload(store, input) {
|
|
12
12
|
store.db
|
|
13
13
|
.prepare(`INSERT INTO local_accepted_uploads (
|
|
14
|
-
binding_id, commit_id, handoff_id,
|
|
14
|
+
binding_id, commit_id, handoff_id, project_sequence, manifest_sha256, created_at_ms
|
|
15
15
|
) VALUES (
|
|
16
|
-
:binding_id, :commit_id, :handoff_id, :
|
|
16
|
+
:binding_id, :commit_id, :handoff_id, :project_sequence, :manifest_sha256, :created_at_ms
|
|
17
17
|
)
|
|
18
18
|
ON CONFLICT(binding_id, commit_id) DO NOTHING`)
|
|
19
19
|
.run({
|
|
20
20
|
binding_id: input.bindingId,
|
|
21
21
|
commit_id: input.commitId,
|
|
22
22
|
handoff_id: input.handoffId,
|
|
23
|
-
|
|
23
|
+
project_sequence: input.projectSequence,
|
|
24
24
|
manifest_sha256: input.manifestSha256,
|
|
25
25
|
created_at_ms: Date.now(),
|
|
26
26
|
});
|
|
@@ -42,7 +42,7 @@ export function latestAcceptedUpload(store, bindingId) {
|
|
|
42
42
|
const row = store.db
|
|
43
43
|
.prepare(`SELECT * FROM local_accepted_uploads
|
|
44
44
|
WHERE binding_id = :binding_id
|
|
45
|
-
ORDER BY
|
|
45
|
+
ORDER BY project_sequence DESC, created_at_ms DESC
|
|
46
46
|
LIMIT 1`)
|
|
47
47
|
.get({ binding_id: bindingId });
|
|
48
48
|
return row ? mapRow(row) : null;
|
|
@@ -3,7 +3,7 @@ import { LIVE_BINDING_STATUSES } from "./sync_vocabulary.js";
|
|
|
3
3
|
function mapRow(row) {
|
|
4
4
|
return {
|
|
5
5
|
id: row.id,
|
|
6
|
-
|
|
6
|
+
projectId: row.project_id,
|
|
7
7
|
organizationId: row.organization_id,
|
|
8
8
|
repositoryRoot: row.repository_root,
|
|
9
9
|
gitDir: row.git_dir,
|
|
@@ -11,7 +11,7 @@ function mapRow(row) {
|
|
|
11
11
|
repositoryFingerprint: row.repository_fingerprint,
|
|
12
12
|
boundBranch: row.bound_branch,
|
|
13
13
|
lastProcessedCommitId: row.last_processed_commit_id ?? null,
|
|
14
|
-
|
|
14
|
+
lastAppliedProjectSequence: row.last_applied_project_sequence,
|
|
15
15
|
whitelistManifestId: row.whitelist_manifest_id ?? null,
|
|
16
16
|
whitelistManifestSha256: row.whitelist_manifest_sha256 ?? null,
|
|
17
17
|
whitelistCachePath: row.whitelist_cache_path ?? null,
|
|
@@ -28,21 +28,21 @@ export function insertBinding(store, input) {
|
|
|
28
28
|
const now = Date.now();
|
|
29
29
|
store.db
|
|
30
30
|
.prepare(`INSERT INTO local_bindings (
|
|
31
|
-
id,
|
|
31
|
+
id, project_id, organization_id, repository_root, git_dir, git_common_dir,
|
|
32
32
|
repository_fingerprint, bound_branch, last_processed_commit_id,
|
|
33
|
-
|
|
33
|
+
last_applied_project_sequence, whitelist_manifest_id, whitelist_manifest_sha256,
|
|
34
34
|
whitelist_cache_path, etna_profile_json, status, last_error_code,
|
|
35
35
|
last_error_detail, created_at_ms, updated_at_ms
|
|
36
36
|
) VALUES (
|
|
37
|
-
:id, :
|
|
37
|
+
:id, :project_id, :organization_id, :repository_root, :git_dir, :git_common_dir,
|
|
38
38
|
:repository_fingerprint, :bound_branch, :last_processed_commit_id,
|
|
39
|
-
:
|
|
39
|
+
:last_applied_project_sequence, :whitelist_manifest_id, :whitelist_manifest_sha256,
|
|
40
40
|
:whitelist_cache_path, :etna_profile_json, :status, NULL,
|
|
41
41
|
NULL, :created_at_ms, :updated_at_ms
|
|
42
42
|
)`)
|
|
43
43
|
.run({
|
|
44
44
|
id: input.id,
|
|
45
|
-
|
|
45
|
+
project_id: input.projectId,
|
|
46
46
|
organization_id: input.organizationId,
|
|
47
47
|
repository_root: input.repositoryRoot,
|
|
48
48
|
git_dir: input.gitDir,
|
|
@@ -50,7 +50,7 @@ export function insertBinding(store, input) {
|
|
|
50
50
|
repository_fingerprint: input.repositoryFingerprint,
|
|
51
51
|
bound_branch: input.boundBranch,
|
|
52
52
|
last_processed_commit_id: input.lastProcessedCommitId ?? null,
|
|
53
|
-
|
|
53
|
+
last_applied_project_sequence: input.lastAppliedProjectSequence ?? 0,
|
|
54
54
|
whitelist_manifest_id: input.whitelistManifestId ?? null,
|
|
55
55
|
whitelist_manifest_sha256: input.whitelistManifestSha256 ?? null,
|
|
56
56
|
whitelist_cache_path: input.whitelistCachePath ?? null,
|
|
@@ -103,12 +103,12 @@ export function updateBindingCursor(store, bindingId, patch) {
|
|
|
103
103
|
WHERE id = :id`)
|
|
104
104
|
.run({ commit: patch.lastProcessedCommitId ?? null, now, id: bindingId });
|
|
105
105
|
}
|
|
106
|
-
if (patch.
|
|
106
|
+
if (patch.lastAppliedProjectSequence !== undefined) {
|
|
107
107
|
store.db
|
|
108
108
|
.prepare(`UPDATE local_bindings
|
|
109
|
-
SET
|
|
109
|
+
SET last_applied_project_sequence = :seq, updated_at_ms = :now
|
|
110
110
|
WHERE id = :id`)
|
|
111
|
-
.run({ seq: patch.
|
|
111
|
+
.run({ seq: patch.lastAppliedProjectSequence, now, id: bindingId });
|
|
112
112
|
}
|
|
113
113
|
return getBindingById(store, bindingId);
|
|
114
114
|
});
|
|
@@ -10,5 +10,5 @@ export * from "./recovery_bundles.js";
|
|
|
10
10
|
export * from "./pending_uploads.js";
|
|
11
11
|
export * from "./history_decisions.js";
|
|
12
12
|
export * from "./accepted_uploads.js";
|
|
13
|
-
export * from "./
|
|
13
|
+
export * from "./project_events_cache.js";
|
|
14
14
|
export * from "./filebeat_state.js";
|
|
@@ -193,6 +193,18 @@ const MIGRATION_V6 = {
|
|
|
193
193
|
`CREATE INDEX local_bindings_workspace_idx ON local_bindings (workspace_id)`,
|
|
194
194
|
],
|
|
195
195
|
};
|
|
196
|
+
const MIGRATION_V7 = {
|
|
197
|
+
version: 7,
|
|
198
|
+
statements: [
|
|
199
|
+
`ALTER TABLE local_bindings RENAME COLUMN workspace_id TO project_id`,
|
|
200
|
+
`ALTER TABLE local_bindings RENAME COLUMN last_applied_workspace_sequence TO last_applied_project_sequence`,
|
|
201
|
+
`ALTER TABLE cached_workspace_events RENAME COLUMN workspace_sequence TO project_sequence`,
|
|
202
|
+
`ALTER TABLE local_accepted_uploads RENAME COLUMN workspace_sequence TO project_sequence`,
|
|
203
|
+
`ALTER TABLE cached_workspace_events RENAME TO cached_project_events`,
|
|
204
|
+
`DROP INDEX IF EXISTS local_bindings_workspace_idx`,
|
|
205
|
+
`CREATE INDEX local_bindings_project_idx ON local_bindings (project_id)`,
|
|
206
|
+
],
|
|
207
|
+
};
|
|
196
208
|
export const MIGRATIONS = [
|
|
197
209
|
MIGRATION_V1,
|
|
198
210
|
MIGRATION_V2,
|
|
@@ -200,6 +212,7 @@ export const MIGRATIONS = [
|
|
|
200
212
|
MIGRATION_V4,
|
|
201
213
|
MIGRATION_V5,
|
|
202
214
|
MIGRATION_V6,
|
|
215
|
+
MIGRATION_V7,
|
|
203
216
|
];
|
|
204
217
|
export const LATEST_SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1].version;
|
|
205
218
|
export class LocalSchemaTooNewError extends Error {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export const DEFAULT_PROJECT_EVENT_KEEP_COUNT = 500;
|
|
2
|
+
function mapRow(row) {
|
|
3
|
+
return {
|
|
4
|
+
bindingId: row.binding_id,
|
|
5
|
+
projectSequence: row.project_sequence,
|
|
6
|
+
eventType: row.event_type,
|
|
7
|
+
payloadJson: row.payload_json ?? null,
|
|
8
|
+
appliedAtMs: row.applied_at_ms ?? null,
|
|
9
|
+
createdAtMs: row.created_at_ms,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export function cacheProjectEvents(store, bindingId, events) {
|
|
13
|
+
const now = Date.now();
|
|
14
|
+
const insert = store.db.prepare(`INSERT INTO cached_project_events (
|
|
15
|
+
binding_id, project_sequence, event_type, payload_json, applied_at_ms, created_at_ms
|
|
16
|
+
) VALUES (
|
|
17
|
+
:binding_id, :project_sequence, :event_type, :payload_json, :applied_at_ms, :created_at_ms
|
|
18
|
+
)
|
|
19
|
+
ON CONFLICT(binding_id, project_sequence) DO NOTHING`);
|
|
20
|
+
for (const event of events) {
|
|
21
|
+
insert.run({
|
|
22
|
+
binding_id: bindingId,
|
|
23
|
+
project_sequence: event.projectSequence,
|
|
24
|
+
event_type: event.eventType,
|
|
25
|
+
payload_json: event.payloadJson ?? null,
|
|
26
|
+
applied_at_ms: event.appliedAtMs ?? null,
|
|
27
|
+
created_at_ms: now,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function latestCachedSequence(store, bindingId) {
|
|
32
|
+
const row = store.db
|
|
33
|
+
.prepare("SELECT MAX(project_sequence) AS latest FROM cached_project_events WHERE binding_id = :binding_id")
|
|
34
|
+
.get({ binding_id: bindingId });
|
|
35
|
+
return row?.latest ?? null;
|
|
36
|
+
}
|
|
37
|
+
export function listCachedProjectEvents(store, bindingId) {
|
|
38
|
+
const rows = store.db
|
|
39
|
+
.prepare("SELECT * FROM cached_project_events WHERE binding_id = :binding_id ORDER BY project_sequence ASC")
|
|
40
|
+
.all({ binding_id: bindingId });
|
|
41
|
+
return rows.map(mapRow);
|
|
42
|
+
}
|
|
43
|
+
export function pruneProjectEvents(store, bindingId, keepCount = DEFAULT_PROJECT_EVENT_KEEP_COUNT) {
|
|
44
|
+
store.db
|
|
45
|
+
.prepare(`DELETE FROM cached_project_events
|
|
46
|
+
WHERE binding_id = :binding_id
|
|
47
|
+
AND project_sequence NOT IN (
|
|
48
|
+
SELECT project_sequence FROM cached_project_events
|
|
49
|
+
WHERE binding_id = :binding_id
|
|
50
|
+
ORDER BY project_sequence DESC
|
|
51
|
+
LIMIT :keep
|
|
52
|
+
)`)
|
|
53
|
+
.run({ binding_id: bindingId, keep: keepCount });
|
|
54
|
+
}
|
|
@@ -60,7 +60,7 @@ export async function exchangeServiceCredential(baseUrl, accessToken, params) {
|
|
|
60
60
|
const response = await authorizedFetch(`${baseUrl}/api/v1/keys/exchange`, {
|
|
61
61
|
method: "POST",
|
|
62
62
|
headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
|
|
63
|
-
body: JSON.stringify({
|
|
63
|
+
body: JSON.stringify({ projectId: params.projectId, service: params.service }),
|
|
64
64
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
65
65
|
});
|
|
66
66
|
const payload = (await response.json().catch(() => null));
|
|
@@ -71,7 +71,7 @@ export async function exchangeServiceCredential(baseUrl, accessToken, params) {
|
|
|
71
71
|
}
|
|
72
72
|
export async function fetchServiceCredential(baseUrl, accessToken, params) {
|
|
73
73
|
const url = new URL(`${baseUrl}/api/v1/keys/credentials`);
|
|
74
|
-
url.searchParams.set("
|
|
74
|
+
url.searchParams.set("projectId", assertSafeOpaqueId("The Project id", params.projectId));
|
|
75
75
|
url.searchParams.set("service", assertSafeOpaqueId("The Service slug", params.service));
|
|
76
76
|
const response = await authorizedFetch(url, {
|
|
77
77
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
@@ -83,9 +83,9 @@ export async function fetchServiceCredential(baseUrl, accessToken, params) {
|
|
|
83
83
|
}
|
|
84
84
|
return narrowProjection(payload, params.service);
|
|
85
85
|
}
|
|
86
|
-
export async function
|
|
86
|
+
export async function listProjectIntegrations(baseUrl, accessToken, projectId) {
|
|
87
87
|
const url = new URL(`${baseUrl}/api/v1/keys/integrations`);
|
|
88
|
-
url.searchParams.set("
|
|
88
|
+
url.searchParams.set("projectId", assertSafeOpaqueId("The Project id", projectId));
|
|
89
89
|
const response = await authorizedFetch(url, {
|
|
90
90
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
91
91
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
@@ -101,7 +101,7 @@ export async function fetchBrokeredCredential(baseUrl, accessToken, params) {
|
|
|
101
101
|
const response = await authorizedFetch(`${baseUrl}/api/v1/keys/broker/${encodeURIComponent(service)}`, {
|
|
102
102
|
method: "POST",
|
|
103
103
|
headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
|
|
104
|
-
body: JSON.stringify({
|
|
104
|
+
body: JSON.stringify({ projectId: params.projectId }),
|
|
105
105
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
106
106
|
});
|
|
107
107
|
const payload = (await response.json().catch(() => null));
|
|
@@ -110,8 +110,8 @@ export async function fetchBrokeredCredential(baseUrl, accessToken, params) {
|
|
|
110
110
|
}
|
|
111
111
|
return { url: payload.url ?? null, key: payload.key, meta: payload.meta ?? null, grantId: payload.grantId };
|
|
112
112
|
}
|
|
113
|
-
export async function
|
|
114
|
-
const url = new URL(`${baseUrl}/public/v1/
|
|
113
|
+
export async function searchProjects(baseUrl, query, opts = {}) {
|
|
114
|
+
const url = new URL(`${baseUrl}/public/v1/projects`);
|
|
115
115
|
url.searchParams.set("q", query);
|
|
116
116
|
if (opts.category)
|
|
117
117
|
url.searchParams.set("category", opts.category);
|
|
@@ -120,7 +120,7 @@ export async function searchWorkspaces(baseUrl, query, opts = {}) {
|
|
|
120
120
|
const response = await authorizedFetch(url, { signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
|
|
121
121
|
const payload = (await response.json().catch(() => null));
|
|
122
122
|
if (!response.ok || !Array.isArray(payload?.data?.items)) {
|
|
123
|
-
throw new Error(errorMessage(payload, response.status, "
|
|
123
|
+
throw new Error(errorMessage(payload, response.status, "project search failed"));
|
|
124
124
|
}
|
|
125
125
|
return payload.data.items;
|
|
126
126
|
}
|
|
@@ -190,10 +190,10 @@ export async function acknowledgeBindingCursor(baseUrl, accessToken, params) {
|
|
|
190
190
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
191
191
|
});
|
|
192
192
|
const payload = (await response.json().catch(() => null));
|
|
193
|
-
if (!response.ok || typeof payload?.data?.
|
|
193
|
+
if (!response.ok || typeof payload?.data?.lastAppliedProjectSequence !== "number") {
|
|
194
194
|
throw new Error(errorMessage(payload, response.status, "cursor acknowledgement failed"));
|
|
195
195
|
}
|
|
196
|
-
return payload.data.
|
|
196
|
+
return payload.data.lastAppliedProjectSequence;
|
|
197
197
|
}
|
|
198
198
|
export async function fetchSyncWhitelist(baseUrl, accessToken, bindingId) {
|
|
199
199
|
const safeId = assertSafeOpaqueId("The binding id", bindingId);
|
|
@@ -295,31 +295,31 @@ export async function createOrganization(baseUrl, accessToken, params, idempoten
|
|
|
295
295
|
}
|
|
296
296
|
return payload.data;
|
|
297
297
|
}
|
|
298
|
-
export async function
|
|
298
|
+
export async function listProjects(baseUrl, accessToken, organizationId) {
|
|
299
299
|
const safeId = assertSafeOpaqueId("The Organization id", organizationId);
|
|
300
|
-
const response = await authorizedFetch(`${baseUrl}/api/v2/
|
|
300
|
+
const response = await authorizedFetch(`${baseUrl}/api/v2/projects`, {
|
|
301
301
|
headers: { ...authHeaders(accessToken), "X-NuStack-Organization": safeId },
|
|
302
302
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
303
303
|
});
|
|
304
304
|
const payload = (await response.json().catch(() => null));
|
|
305
305
|
if (!response.ok || !Array.isArray(payload?.data?.items)) {
|
|
306
|
-
throw new Error(errorMessage(payload, response.status, "
|
|
306
|
+
throw new Error(errorMessage(payload, response.status, "project list failed"));
|
|
307
307
|
}
|
|
308
308
|
return payload.data.items;
|
|
309
309
|
}
|
|
310
|
-
export async function
|
|
311
|
-
const safeId = assertSafeOpaqueId("The
|
|
312
|
-
const response = await authorizedFetch(`${baseUrl}/api/v2/
|
|
310
|
+
export async function getProject(baseUrl, accessToken, projectId) {
|
|
311
|
+
const safeId = assertSafeOpaqueId("The Project id", projectId);
|
|
312
|
+
const response = await authorizedFetch(`${baseUrl}/api/v2/projects/${encodeURIComponent(safeId)}`, {
|
|
313
313
|
headers: authHeaders(accessToken),
|
|
314
314
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
315
315
|
});
|
|
316
316
|
const payload = (await response.json().catch(() => null));
|
|
317
317
|
if (!response.ok || !payload?.data?.id) {
|
|
318
|
-
throw new Error(errorMessage(payload, response.status, "
|
|
318
|
+
throw new Error(errorMessage(payload, response.status, "project fetch failed"));
|
|
319
319
|
}
|
|
320
320
|
return payload.data;
|
|
321
321
|
}
|
|
322
|
-
export function
|
|
322
|
+
export function mintProjectId() {
|
|
323
323
|
const bytes = crypto.getRandomValues(new Uint8Array(16));
|
|
324
324
|
const ms = Date.now();
|
|
325
325
|
bytes[0] = Math.floor(ms / 2 ** 40) & 0xff;
|
|
@@ -360,9 +360,9 @@ async function* readSseFrames(body) {
|
|
|
360
360
|
for (const block of parser.flush())
|
|
361
361
|
yield toFrame(block);
|
|
362
362
|
}
|
|
363
|
-
export async function
|
|
363
|
+
export async function createProjectViaAsk(baseUrl, accessToken, organizationId, prompt, opts = {}) {
|
|
364
364
|
const safeOrganization = assertSafeOpaqueId("The Organization id", organizationId);
|
|
365
|
-
const
|
|
365
|
+
const projectId = opts.projectId ?? mintProjectId();
|
|
366
366
|
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
367
367
|
const response = await fetchImpl(`${baseUrl}/api/v2/ask`, {
|
|
368
368
|
method: "POST",
|
|
@@ -371,11 +371,11 @@ export async function createWorkspaceViaAsk(baseUrl, accessToken, organizationId
|
|
|
371
371
|
"Content-Type": "application/json",
|
|
372
372
|
"X-NuStack-Organization": safeOrganization,
|
|
373
373
|
},
|
|
374
|
-
body: JSON.stringify({ prompt,
|
|
374
|
+
body: JSON.stringify({ prompt, projectId }),
|
|
375
375
|
});
|
|
376
376
|
if (!response.ok || !response.body) {
|
|
377
377
|
const payload = (await response.json().catch(() => null));
|
|
378
|
-
throw new Error(errorMessage(payload, response.status, "
|
|
378
|
+
throw new Error(errorMessage(payload, response.status, "project create failed"));
|
|
379
379
|
}
|
|
380
380
|
let started = false;
|
|
381
381
|
let turnId = null;
|
|
@@ -384,7 +384,7 @@ export async function createWorkspaceViaAsk(baseUrl, accessToken, organizationId
|
|
|
384
384
|
if (frame.event === "start") {
|
|
385
385
|
started = true;
|
|
386
386
|
turnId = typeof frame.data.turnId === "string" ? frame.data.turnId : null;
|
|
387
|
-
opts.onPhase?.("
|
|
387
|
+
opts.onPhase?.("Project created — writing the first document");
|
|
388
388
|
}
|
|
389
389
|
else if (frame.event === "thinking" && typeof frame.data.message === "string") {
|
|
390
390
|
opts.onPhase?.(frame.data.message);
|
|
@@ -392,7 +392,7 @@ export async function createWorkspaceViaAsk(baseUrl, accessToken, organizationId
|
|
|
392
392
|
else if (frame.event === "error") {
|
|
393
393
|
const message = typeof frame.data.message === "string" ? frame.data.message : "generation failed";
|
|
394
394
|
if (!started)
|
|
395
|
-
throw new Error(`
|
|
395
|
+
throw new Error(`project create failed: ${message}`);
|
|
396
396
|
generationError = message;
|
|
397
397
|
break;
|
|
398
398
|
}
|
|
@@ -401,12 +401,12 @@ export async function createWorkspaceViaAsk(baseUrl, accessToken, organizationId
|
|
|
401
401
|
}
|
|
402
402
|
}
|
|
403
403
|
if (!started)
|
|
404
|
-
throw new Error("
|
|
405
|
-
return {
|
|
404
|
+
throw new Error("project create failed: the stream ended before the Project was reported");
|
|
405
|
+
return { projectId, turnId, generationError };
|
|
406
406
|
}
|
|
407
|
-
async function
|
|
408
|
-
const safeId = assertSafeOpaqueId("The
|
|
409
|
-
const response = await authorizedFetch(`${baseUrl}/api/v2/
|
|
407
|
+
async function postProjectPublication(baseUrl, accessToken, projectId, action, body, idempotencyKey) {
|
|
408
|
+
const safeId = assertSafeOpaqueId("The Project id", projectId);
|
|
409
|
+
const response = await authorizedFetch(`${baseUrl}/api/v2/projects/${encodeURIComponent(safeId)}/${action}`, {
|
|
410
410
|
method: "POST",
|
|
411
411
|
headers: mutationHeaders(accessToken, idempotencyKey),
|
|
412
412
|
body: JSON.stringify(body),
|
|
@@ -414,19 +414,19 @@ async function postWorkspacePublication(baseUrl, accessToken, workspaceId, actio
|
|
|
414
414
|
});
|
|
415
415
|
const payload = (await response.json().catch(() => null));
|
|
416
416
|
if (!response.ok || payload?.data === undefined) {
|
|
417
|
-
throw new Error(errorMessage(payload, response.status, `
|
|
417
|
+
throw new Error(errorMessage(payload, response.status, `project ${action} failed`));
|
|
418
418
|
}
|
|
419
419
|
return payload.data;
|
|
420
420
|
}
|
|
421
|
-
export async function
|
|
422
|
-
return
|
|
421
|
+
export async function publishProject(baseUrl, accessToken, projectId, profile, idempotencyKey) {
|
|
422
|
+
return postProjectPublication(baseUrl, accessToken, projectId, "publish", profile, idempotencyKey);
|
|
423
423
|
}
|
|
424
|
-
export async function
|
|
425
|
-
return
|
|
424
|
+
export async function unpublishProject(baseUrl, accessToken, projectId, idempotencyKey) {
|
|
425
|
+
return postProjectPublication(baseUrl, accessToken, projectId, "unpublish", {}, idempotencyKey);
|
|
426
426
|
}
|
|
427
|
-
export async function listDocuments(baseUrl, accessToken,
|
|
428
|
-
const safeId = assertSafeOpaqueId("The
|
|
429
|
-
const response = await authorizedFetch(`${baseUrl}/api/v2/documents?
|
|
427
|
+
export async function listDocuments(baseUrl, accessToken, projectId) {
|
|
428
|
+
const safeId = assertSafeOpaqueId("The Project id", projectId);
|
|
429
|
+
const response = await authorizedFetch(`${baseUrl}/api/v2/documents?projectId=${encodeURIComponent(safeId)}`, {
|
|
430
430
|
headers: authHeaders(accessToken),
|
|
431
431
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
432
432
|
});
|
|
@@ -448,9 +448,9 @@ export async function getDocument(baseUrl, accessToken, documentId) {
|
|
|
448
448
|
}
|
|
449
449
|
return payload.data;
|
|
450
450
|
}
|
|
451
|
-
export async function listHandoffs(baseUrl, accessToken,
|
|
452
|
-
const safeId = assertSafeOpaqueId("The
|
|
453
|
-
const response = await authorizedFetch(`${baseUrl}/api/v2/
|
|
451
|
+
export async function listHandoffs(baseUrl, accessToken, projectId) {
|
|
452
|
+
const safeId = assertSafeOpaqueId("The Project id", projectId);
|
|
453
|
+
const response = await authorizedFetch(`${baseUrl}/api/v2/projects/${encodeURIComponent(safeId)}/handoffs`, {
|
|
454
454
|
headers: authHeaders(accessToken),
|
|
455
455
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
456
456
|
});
|
|
@@ -472,13 +472,13 @@ export async function getHandoff(baseUrl, accessToken, handoffId) {
|
|
|
472
472
|
}
|
|
473
473
|
return payload.data;
|
|
474
474
|
}
|
|
475
|
-
export async function latestHandoff(baseUrl, accessToken,
|
|
476
|
-
const handoffs = await listHandoffs(baseUrl, accessToken,
|
|
475
|
+
export async function latestHandoff(baseUrl, accessToken, projectId) {
|
|
476
|
+
const handoffs = await listHandoffs(baseUrl, accessToken, projectId);
|
|
477
477
|
return handoffs.length > 0 ? handoffs[0] : null;
|
|
478
478
|
}
|
|
479
|
-
export async function createBinding(baseUrl, accessToken,
|
|
480
|
-
const safeId = assertSafeOpaqueId("The
|
|
481
|
-
const response = await authorizedFetch(`${baseUrl}/api/v2/
|
|
479
|
+
export async function createBinding(baseUrl, accessToken, projectId, input, idempotencyKey) {
|
|
480
|
+
const safeId = assertSafeOpaqueId("The Project id", projectId);
|
|
481
|
+
const response = await authorizedFetch(`${baseUrl}/api/v2/projects/${encodeURIComponent(safeId)}/bindings`, {
|
|
482
482
|
method: "POST",
|
|
483
483
|
headers: mutationHeaders(accessToken, idempotencyKey),
|
|
484
484
|
body: JSON.stringify({
|