@nurix/nustack 0.10.0-dev.9 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +1 -0
  2. package/dist/commands/bootstrap.js +4 -4
  3. package/dist/commands/docs.js +13 -13
  4. package/dist/commands/doctor.js +51 -17
  5. package/dist/commands/handoff.js +8 -8
  6. package/dist/commands/init.js +43 -43
  7. package/dist/commands/login.js +19 -6
  8. package/dist/commands/logout.js +14 -6
  9. package/dist/commands/logscope.js +55 -0
  10. package/dist/commands/{workspace-sync.js → project-sync.js} +5 -5
  11. package/dist/commands/{workspace.js → project.js} +133 -133
  12. package/dist/commands/review.js +38 -15
  13. package/dist/commands/search.js +7 -7
  14. package/dist/commands/sessions.js +156 -5
  15. package/dist/commands/sync.js +10 -10
  16. package/dist/commands/token.js +102 -0
  17. package/dist/commands/toolkit.js +19 -19
  18. package/dist/index.js +88 -36
  19. package/dist/lib/auth_grant.js +32 -0
  20. package/dist/lib/auth_recovery.js +3 -1
  21. package/dist/lib/credential_store.js +106 -0
  22. package/dist/lib/device_identity.js +6 -6
  23. package/dist/lib/device_keychain.js +16 -37
  24. package/dist/lib/etna/profile.js +9 -9
  25. package/dist/lib/etna/reconcile.js +11 -11
  26. package/dist/lib/git-sync/core/engine.js +4 -4
  27. package/dist/lib/git-sync/inbound/apply_commit.js +3 -3
  28. package/dist/lib/git-sync/inbound/approval.js +1 -1
  29. package/dist/lib/git-sync/inbound/download.js +8 -8
  30. package/dist/lib/git-sync/inbound/event_sync.js +13 -13
  31. package/dist/lib/git-sync/inbound/notification.js +1 -1
  32. package/dist/lib/git-sync/outbound/extraction.js +4 -4
  33. package/dist/lib/git-sync/outbound/history_decision.js +2 -2
  34. package/dist/lib/git-sync/outbound/publisher.js +2 -2
  35. package/dist/lib/git-sync/outbound/queue.js +3 -3
  36. package/dist/lib/git-sync/outbound/upload_client.js +4 -4
  37. package/dist/lib/git-sync/whitelist/index.js +1 -1
  38. package/dist/lib/git-sync/whitelist/types.js +1 -1
  39. package/dist/lib/git-sync/whitelist/validation.js +5 -5
  40. package/dist/lib/libsecret_store.js +47 -0
  41. package/dist/lib/local_store/accepted_uploads.js +5 -5
  42. package/dist/lib/local_store/bindings.js +11 -11
  43. package/dist/lib/local_store/index.js +1 -1
  44. package/dist/lib/local_store/migrations.js +13 -0
  45. package/dist/lib/local_store/project_events_cache.js +54 -0
  46. package/dist/lib/logs/answer.js +99 -0
  47. package/dist/lib/logs/library.js +19 -0
  48. package/dist/lib/logs/lifecycle.js +159 -0
  49. package/dist/lib/nustack_client.js +88 -46
  50. package/dist/lib/onboarding/attach_existing_git.js +7 -7
  51. package/dist/lib/onboarding/binding.js +11 -11
  52. package/dist/lib/onboarding/folder_state.js +10 -10
  53. package/dist/lib/onboarding/handoff_apply.js +2 -2
  54. package/dist/lib/onboarding/init_fresh_folder.js +18 -18
  55. package/dist/lib/onboarding/manifest_writer.js +31 -31
  56. package/dist/lib/onboarding/web_first_attach.js +12 -12
  57. package/dist/lib/{workspace_context.js → project_context.js} +24 -24
  58. package/dist/lib/review/client.js +1 -1
  59. package/dist/lib/{lanes → session-engine}/claudeAdapter.js +3 -3
  60. package/dist/lib/{lanes → session-engine}/frames.js +36 -27
  61. package/dist/lib/session-engine/prune.js +233 -0
  62. package/dist/lib/session-engine/registry.js +83 -0
  63. package/dist/lib/session-engine/supervisor.js +321 -0
  64. package/dist/lib/session-engine/worktree.js +89 -0
  65. package/dist/lib/session_telemetry.js +2 -2
  66. package/dist/lib/sessions/client.js +8 -8
  67. package/dist/lib/sessions/outsideSessions.js +6 -6
  68. package/dist/lib/telemetry/config_renderer.js +3 -3
  69. package/dist/lib/telemetry/health_report.js +1 -1
  70. package/package.json +3 -2
  71. package/dist/commands/lane.js +0 -12
  72. package/dist/lib/lanes/supervisor.js +0 -247
  73. package/dist/lib/lanes/worktree.js +0 -33
  74. package/dist/lib/local_store/workspace_events_cache.js +0 -54
@@ -11,12 +11,12 @@ import { readTargetState } from "./content_decision.js";
11
11
  import { computeStateFingerprint } from "./state_fingerprint.js";
12
12
  import { persistRecoveryBundle } from "../recovery/store.js";
13
13
  export const NUSTACK_SYNC_IDENTITY = { name: "NuStack Sync", email: "sync@nustack.local" };
14
- export function buildCommitMessage(handoffId, workspaceId, approvedByUserId) {
14
+ export function buildCommitMessage(handoffId, projectId, approvedByUserId) {
15
15
  const lines = [
16
16
  `nustack(sync): apply cloud handoff ${handoffId.slice(0, 12)}`,
17
17
  "",
18
18
  `NuStack-Handoff: ${handoffId}`,
19
- `NuStack-Workspace: ${workspaceId}`,
19
+ `NuStack-Project: ${projectId}`,
20
20
  "NuStack-Origin: cloud",
21
21
  ];
22
22
  if (approvedByUserId)
@@ -186,7 +186,7 @@ export async function applyCommit(input) {
186
186
  if (target.operation === "upsert" && target.blobPath)
187
187
  bytesByPath.set(target.path, readFileSync(target.blobPath));
188
188
  }
189
- const message = buildCommitMessage(handoffId, manifest.workspaceId, approved ? (input.approvedByUserId ?? null) : null);
189
+ const message = buildCommitMessage(handoffId, manifest.projectId, approved ? (input.approvedByUserId ?? null) : null);
190
190
  const built = buildCommit(repo, oldHead, targets, bytesByPath, message, {
191
191
  authorName: NUSTACK_SYNC_IDENTITY.name,
192
192
  authorEmail: NUSTACK_SYNC_IDENTITY.email,
@@ -12,7 +12,7 @@ export const APPROVAL_RECOVERY_NOTE = "Approving overwrites the listed paths. Gi
12
12
  export function toDownloadBinding(binding) {
13
13
  return {
14
14
  id: binding.id,
15
- workspaceId: binding.workspaceId,
15
+ projectId: binding.projectId,
16
16
  boundBranch: binding.boundBranch,
17
17
  repositoryRoot: binding.repositoryRoot,
18
18
  etnaProfileJson: binding.etnaProfileJson,
@@ -27,7 +27,7 @@ function recomputeManifestHash(manifest) {
27
27
  const core = {
28
28
  schemaVersion: 1,
29
29
  handoffId: manifest.handoffId,
30
- workspaceId: manifest.workspaceId,
30
+ projectId: manifest.projectId,
31
31
  documentSetVersionId: manifest.documentSetVersionId,
32
32
  toolkit: {
33
33
  name: manifest.toolkit.name,
@@ -62,7 +62,7 @@ function toolkitTechnicalWait(binding, manifest) {
62
62
  return "harness_incompatible";
63
63
  return null;
64
64
  }
65
- function crossWorkspaceMarker(body, workspaceId) {
65
+ function crossProjectMarker(body, projectId) {
66
66
  if (!body.startsWith("---\n"))
67
67
  return false;
68
68
  const secondLineEnd = body.indexOf("\n", 4);
@@ -71,8 +71,8 @@ function crossWorkspaceMarker(body, workspaceId) {
71
71
  return false;
72
72
  const fence = body.indexOf("\n---", 4);
73
73
  const block = body.slice(0, fence === -1 ? body.length : fence);
74
- const match = /\n\s*workspace_id:\s*(\S+)/.exec(block);
75
- return match !== null && match[1] !== workspaceId;
74
+ const match = /\n\s*project_id:\s*(\S+)/.exec(block);
75
+ return match !== null && match[1] !== projectId;
76
76
  }
77
77
  function isUnsupportedMedia(bytes) {
78
78
  for (const byte of bytes)
@@ -136,8 +136,8 @@ export async function downloadHandoff(deps, binding, handoffId) {
136
136
  const downloadContent = deps.downloadContent ?? downloadTargetContent;
137
137
  const fetchWhitelist = deps.fetchWhitelist ?? fetchSyncWhitelist;
138
138
  const manifest = await fetchManifest(deps.baseUrl, deps.accessToken, { handoffId, bindingId: binding.id });
139
- if (manifest.workspaceId !== binding.workspaceId)
140
- return reject("workspace_mismatch", "manifest Workspace does not match the binding");
139
+ if (manifest.projectId !== binding.projectId)
140
+ return reject("project_mismatch", "manifest Project does not match the binding");
141
141
  if (recomputeManifestHash(manifest) !== manifest.manifestSha256) {
142
142
  return reject("manifest_hash", "manifest hash does not verify");
143
143
  }
@@ -207,8 +207,8 @@ export async function downloadHandoff(deps, binding, handoffId) {
207
207
  const actualSha256 = sha256(bytes);
208
208
  if (actualSha256 !== target.targetSha256)
209
209
  return reject("hash_mismatch", "downloaded body hash does not match the manifest");
210
- if (crossWorkspaceMarker(new TextDecoder().decode(bytes), binding.workspaceId)) {
211
- return reject("cross_workspace_marker", "target body declares a foreign Workspace id");
210
+ if (crossProjectMarker(new TextDecoder().decode(bytes), binding.projectId)) {
211
+ return reject("cross_project_marker", "target body declares a foreign Project id");
212
212
  }
213
213
  const blobPath = blobCachePath(actualSha256);
214
214
  await mkdirSecure(path.dirname(blobPath));
@@ -1,6 +1,6 @@
1
1
  import { getBindingById, updateBindingCursor } from "../../local_store/bindings.js";
2
2
  import { hasCloudOriginForHandoff } from "../../local_store/commit_origins.js";
3
- import { cacheWorkspaceEvents, listCachedWorkspaceEvents, pruneWorkspaceEvents, } from "../../local_store/workspace_events_cache.js";
3
+ import { cacheProjectEvents, listCachedProjectEvents, pruneProjectEvents, } from "../../local_store/project_events_cache.js";
4
4
  import { getApplication, upsertApplication, transitionApplicationState, listApplicationsForBinding, } from "../../local_store/handoff_applications.js";
5
5
  import { fetchBindingEvents, acknowledgeBindingCursor } from "../../nustack_client.js";
6
6
  const PENDING_STATES = new Set([
@@ -29,7 +29,7 @@ function supersedeOlderApplications(store, bindingId, newerHandoffId) {
29
29
  return flipped;
30
30
  }
31
31
  function handoffIdOf(event) {
32
- return event.type === "version.created" && event.entity.kind === "workspace_document_version" ? event.entity.id : null;
32
+ return event.type === "version.created" && event.entity.kind === "project_document_version" ? event.entity.id : null;
33
33
  }
34
34
  export async function syncBindingEvents(store, deps, bindingId) {
35
35
  const binding = getBindingById(store, bindingId);
@@ -40,9 +40,9 @@ export async function syncBindingEvents(store, deps, bindingId) {
40
40
  createdHandoffs: [],
41
41
  supersededHandoffs: [],
42
42
  acknowledgedOwnOrigin: [],
43
- highestSequence: binding.lastAppliedWorkspaceSequence,
43
+ highestSequence: binding.lastAppliedProjectSequence,
44
44
  };
45
- let after = binding.lastAppliedWorkspaceSequence;
45
+ let after = binding.lastAppliedProjectSequence;
46
46
  for (;;) {
47
47
  const events = await fetchEvents(deps.baseUrl, deps.accessToken, {
48
48
  bindingId,
@@ -51,8 +51,8 @@ export async function syncBindingEvents(store, deps, bindingId) {
51
51
  });
52
52
  if (events.length === 0)
53
53
  break;
54
- cacheWorkspaceEvents(store, bindingId, events.map((event) => ({
55
- workspaceSequence: event.sequence,
54
+ cacheProjectEvents(store, bindingId, events.map((event) => ({
55
+ projectSequence: event.sequence,
56
56
  eventType: event.type,
57
57
  payloadJson: JSON.stringify({ entity: event.entity, payload: event.payload }),
58
58
  })));
@@ -81,7 +81,7 @@ export async function syncBindingEvents(store, deps, bindingId) {
81
81
  if (events.length < EVENTS_PAGE_LIMIT)
82
82
  break;
83
83
  }
84
- pruneWorkspaceEvents(store, bindingId);
84
+ pruneProjectEvents(store, bindingId);
85
85
  return result;
86
86
  }
87
87
  function cachedHandoffId(eventType, payloadJson) {
@@ -100,9 +100,9 @@ export async function advanceContiguousCursor(store, deps, bindingId) {
100
100
  if (!binding)
101
101
  throw new Error(`No binding "${bindingId}" on this machine.`);
102
102
  const acknowledgeCursor = deps.acknowledgeCursor ?? acknowledgeBindingCursor;
103
- let cursor = binding.lastAppliedWorkspaceSequence;
104
- for (const event of listCachedWorkspaceEvents(store, bindingId)) {
105
- if (event.workspaceSequence <= cursor)
103
+ let cursor = binding.lastAppliedProjectSequence;
104
+ for (const event of listCachedProjectEvents(store, bindingId)) {
105
+ if (event.projectSequence <= cursor)
106
106
  continue;
107
107
  const handoffId = cachedHandoffId(event.eventType, event.payloadJson);
108
108
  if (handoffId !== null) {
@@ -110,10 +110,10 @@ export async function advanceContiguousCursor(store, deps, bindingId) {
110
110
  if (!application || PENDING_STATES.has(application.state))
111
111
  break;
112
112
  }
113
- cursor = event.workspaceSequence;
113
+ cursor = event.projectSequence;
114
114
  }
115
- if (cursor > binding.lastAppliedWorkspaceSequence) {
116
- updateBindingCursor(store, bindingId, { lastAppliedWorkspaceSequence: cursor });
115
+ if (cursor > binding.lastAppliedProjectSequence) {
116
+ updateBindingCursor(store, bindingId, { lastAppliedProjectSequence: cursor });
117
117
  try {
118
118
  await acknowledgeCursor(deps.baseUrl, deps.accessToken, { bindingId, lastAppliedSequence: cursor });
119
119
  }
@@ -7,7 +7,7 @@ export function renderCommitNotification(application, changedPaths) {
7
7
  lines.push(` commit: ${application.generatedCommitId.slice(0, 12)}`);
8
8
  if (changedPaths.length > 0)
9
9
  lines.push(` changed: ${changedPaths.join(", ")}`);
10
- lines.push(` View: nustack workspace sync pending Undo: nustack workspace sync undo ${application.handoffId}`);
10
+ lines.push(` View: nustack project sync pending Undo: nustack project sync undo ${application.handoffId}`);
11
11
  if (application.recoveryBundleId) {
12
12
  lines.push(` Recovery bundle ${application.recoveryBundleId} holds your pre-approval content until it expires.`);
13
13
  }
@@ -43,11 +43,11 @@ function readBlob(repo, blobId) {
43
43
  export function isCommitTreeReadable(repo, commitId) {
44
44
  return runGit(repo.repositoryRoot, ["cat-file", "-e", `${commitId}^{tree}`], { allowFailure: true }).exitCode === 0;
45
45
  }
46
- export function extractCommitDelta(repo, commitId, whitelist, workspaceId, direction = "local_to_cloud") {
46
+ export function extractCommitDelta(repo, commitId, whitelist, projectId, direction = "local_to_cloud") {
47
47
  const parentCommitId = readRefCommit(repo, `${commitId}^1`);
48
- return computeTreeDelta(repo, parentCommitId, commitId, whitelist, workspaceId, direction);
48
+ return computeTreeDelta(repo, parentCommitId, commitId, whitelist, projectId, direction);
49
49
  }
50
- export function computeTreeDelta(repo, baseCommitId, targetCommitId, whitelist, workspaceId, direction = "local_to_cloud") {
50
+ export function computeTreeDelta(repo, baseCommitId, targetCommitId, whitelist, projectId, direction = "local_to_cloud") {
51
51
  const diffArgs = ["diff-tree", "-r", "-z", "--no-renames", "--no-commit-id"];
52
52
  const args = baseCommitId === null ? [...diffArgs, "--root", targetCommitId] : [...diffArgs, baseCommitId, targetCommitId];
53
53
  const { stdout } = runGit(repo.repositoryRoot, args);
@@ -81,7 +81,7 @@ export function computeTreeDelta(repo, baseCommitId, targetCommitId, whitelist,
81
81
  bytes: blob.bytes,
82
82
  byteSize: blob.byteSize,
83
83
  entry: match.entry,
84
- workspaceId,
84
+ projectId,
85
85
  });
86
86
  if (!validation.ok)
87
87
  return { ok: false, code: validation.code, path };
@@ -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
- workspaceId: binding.workspaceId,
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.workspaceId);
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
- workspaceId: binding.workspaceId,
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, workspaceSequence: outcome.workspaceSequence });
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.workspaceId);
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
- workspaceSequence: outcome.accepted.workspaceSequence,
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, workspaceSequence: outcome.accepted.workspaceSequence };
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
- "WORKSPACE_NOT_FOUND",
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 workspaceSequence = data.workspaceSequence ?? response.body?.meta?.workspaceSequence;
124
- if (!data.handoffId || typeof workspaceSequence !== "number") {
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, workspaceSequence } } };
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, frontmatterWorkspaceId, } from "./validation.js";
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.workspaceId === "string" &&
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 frontmatterWorkspaceId(text) {
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*(?:workspace_id|workspaceId|app_id|appId)\s*:\s*(.+?)\s*$/.exec(line);
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 declaredWorkspace = frontmatterWorkspaceId(text);
56
- if (declaredWorkspace !== null && declaredWorkspace !== input.workspaceId) {
57
- return { ok: false, code: "cross_workspace_marker" };
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
  }
@@ -0,0 +1,47 @@
1
+ import { execFile } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { promisify } from "node:util";
4
+ const runFile = promisify(execFile);
5
+ const DEFAULT_SECRET_TOOL_BIN = "/usr/bin/secret-tool";
6
+ const SERVICE = "nustack-device";
7
+ const LABEL = "NuStack device credential";
8
+ function secretToolBin() {
9
+ return process.env.NUSTACK_SECRET_TOOL_BIN?.trim() || DEFAULT_SECRET_TOOL_BIN;
10
+ }
11
+ export function isLibsecretAvailable() {
12
+ if (process.env.NUSTACK_SECRET_TOOL_BIN)
13
+ return true;
14
+ return process.platform === "linux" && existsSync(DEFAULT_SECRET_TOOL_BIN);
15
+ }
16
+ function attributes(account) {
17
+ return ["service", SERVICE, "account", account];
18
+ }
19
+ export const libsecretStore = {
20
+ name: "libsecret",
21
+ async readRaw(account) {
22
+ try {
23
+ const { stdout } = await runFile(secretToolBin(), ["lookup", ...attributes(account)]);
24
+ return stdout.trim() || null;
25
+ }
26
+ catch {
27
+ return null;
28
+ }
29
+ },
30
+ writeRaw(account, payload) {
31
+ return new Promise((resolve, reject) => {
32
+ const child = execFile(secretToolBin(), ["store", `--label=${LABEL}`, ...attributes(account)], (error) => error ? reject(error) : resolve());
33
+ child.stdin?.end(payload);
34
+ });
35
+ },
36
+ async deleteRaw(account) {
37
+ if (!(await this.readRaw(account)))
38
+ return false;
39
+ try {
40
+ await runFile(secretToolBin(), ["clear", ...attributes(account)]);
41
+ return true;
42
+ }
43
+ catch {
44
+ return false;
45
+ }
46
+ },
47
+ };
@@ -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
- workspaceSequence: row.workspace_sequence,
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, workspace_sequence, manifest_sha256, created_at_ms
14
+ binding_id, commit_id, handoff_id, project_sequence, manifest_sha256, created_at_ms
15
15
  ) VALUES (
16
- :binding_id, :commit_id, :handoff_id, :workspace_sequence, :manifest_sha256, :created_at_ms
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
- workspace_sequence: input.workspaceSequence,
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 workspace_sequence DESC, created_at_ms DESC
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
- workspaceId: row.workspace_id,
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
- lastAppliedWorkspaceSequence: row.last_applied_workspace_sequence,
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, workspace_id, organization_id, repository_root, git_dir, git_common_dir,
31
+ id, project_id, organization_id, repository_root, git_dir, git_common_dir,
32
32
  repository_fingerprint, bound_branch, last_processed_commit_id,
33
- last_applied_workspace_sequence, whitelist_manifest_id, whitelist_manifest_sha256,
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, :workspace_id, :organization_id, :repository_root, :git_dir, :git_common_dir,
37
+ :id, :project_id, :organization_id, :repository_root, :git_dir, :git_common_dir,
38
38
  :repository_fingerprint, :bound_branch, :last_processed_commit_id,
39
- :last_applied_workspace_sequence, :whitelist_manifest_id, :whitelist_manifest_sha256,
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
- workspace_id: input.workspaceId,
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
- last_applied_workspace_sequence: input.lastAppliedWorkspaceSequence ?? 0,
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.lastAppliedWorkspaceSequence !== undefined) {
106
+ if (patch.lastAppliedProjectSequence !== undefined) {
107
107
  store.db
108
108
  .prepare(`UPDATE local_bindings
109
- SET last_applied_workspace_sequence = :seq, updated_at_ms = :now
109
+ SET last_applied_project_sequence = :seq, updated_at_ms = :now
110
110
  WHERE id = :id`)
111
- .run({ seq: patch.lastAppliedWorkspaceSequence, now, id: bindingId });
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 "./workspace_events_cache.js";
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
+ }