@nurix/nustack 0.10.0-dev.10

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 (131) hide show
  1. package/README.md +77 -0
  2. package/dist/commands/add.js +130 -0
  3. package/dist/commands/agent.js +100 -0
  4. package/dist/commands/bootstrap.js +54 -0
  5. package/dist/commands/describe.js +34 -0
  6. package/dist/commands/device.js +159 -0
  7. package/dist/commands/docs.js +119 -0
  8. package/dist/commands/doctor.js +284 -0
  9. package/dist/commands/graph.js +34 -0
  10. package/dist/commands/handoff.js +108 -0
  11. package/dist/commands/init.js +280 -0
  12. package/dist/commands/lane.js +12 -0
  13. package/dist/commands/list.js +20 -0
  14. package/dist/commands/login.js +134 -0
  15. package/dist/commands/logout.js +44 -0
  16. package/dist/commands/open.js +54 -0
  17. package/dist/commands/organization.js +114 -0
  18. package/dist/commands/review.js +152 -0
  19. package/dist/commands/search.js +57 -0
  20. package/dist/commands/sessions.js +62 -0
  21. package/dist/commands/sync.js +85 -0
  22. package/dist/commands/telemetry.js +112 -0
  23. package/dist/commands/toolkit.js +167 -0
  24. package/dist/commands/validate.js +67 -0
  25. package/dist/commands/workspace-sync.js +645 -0
  26. package/dist/commands/workspace.js +479 -0
  27. package/dist/index.js +357 -0
  28. package/dist/lib/agent/claudeHooks.js +77 -0
  29. package/dist/lib/auth_grant.js +398 -0
  30. package/dist/lib/auth_recovery.js +49 -0
  31. package/dist/lib/browser_opener.js +24 -0
  32. package/dist/lib/device_identity.js +28 -0
  33. package/dist/lib/device_keychain.js +101 -0
  34. package/dist/lib/env_file.js +75 -0
  35. package/dist/lib/etna/manager.js +43 -0
  36. package/dist/lib/etna/profile.js +41 -0
  37. package/dist/lib/etna/reconcile.js +77 -0
  38. package/dist/lib/filebeat_installer.js +246 -0
  39. package/dist/lib/git-sync/core/commit_range.js +37 -0
  40. package/dist/lib/git-sync/core/engine.js +263 -0
  41. package/dist/lib/git-sync/core/git_runner.js +137 -0
  42. package/dist/lib/git-sync/core/operation_state.js +22 -0
  43. package/dist/lib/git-sync/core/path_normalizer.js +98 -0
  44. package/dist/lib/git-sync/core/reconciliation_lock.js +103 -0
  45. package/dist/lib/git-sync/core/repository.js +49 -0
  46. package/dist/lib/git-sync/core/watch_stream.js +86 -0
  47. package/dist/lib/git-sync/core/watcher.js +61 -0
  48. package/dist/lib/git-sync/inbound/applier.js +131 -0
  49. package/dist/lib/git-sync/inbound/apply_commit.js +225 -0
  50. package/dist/lib/git-sync/inbound/approval.js +117 -0
  51. package/dist/lib/git-sync/inbound/content_decision.js +148 -0
  52. package/dist/lib/git-sync/inbound/download.js +223 -0
  53. package/dist/lib/git-sync/inbound/event_sync.js +133 -0
  54. package/dist/lib/git-sync/inbound/index.js +10 -0
  55. package/dist/lib/git-sync/inbound/notification.js +39 -0
  56. package/dist/lib/git-sync/inbound/state_fingerprint.js +32 -0
  57. package/dist/lib/git-sync/inbound/technical_wait.js +39 -0
  58. package/dist/lib/git-sync/inbound/undo.js +198 -0
  59. package/dist/lib/git-sync/outbound/extraction.js +107 -0
  60. package/dist/lib/git-sync/outbound/history_decision.js +112 -0
  61. package/dist/lib/git-sync/outbound/index.js +26 -0
  62. package/dist/lib/git-sync/outbound/manifest_builder.js +58 -0
  63. package/dist/lib/git-sync/outbound/publisher.js +51 -0
  64. package/dist/lib/git-sync/outbound/queue.js +123 -0
  65. package/dist/lib/git-sync/outbound/state.js +12 -0
  66. package/dist/lib/git-sync/outbound/upload_client.js +141 -0
  67. package/dist/lib/git-sync/recovery/bundle.js +56 -0
  68. package/dist/lib/git-sync/recovery/crypto.js +114 -0
  69. package/dist/lib/git-sync/recovery/index.js +3 -0
  70. package/dist/lib/git-sync/recovery/store.js +55 -0
  71. package/dist/lib/git-sync/whitelist/index.js +4 -0
  72. package/dist/lib/git-sync/whitelist/matcher.js +62 -0
  73. package/dist/lib/git-sync/whitelist/store.js +99 -0
  74. package/dist/lib/git-sync/whitelist/types.js +46 -0
  75. package/dist/lib/git-sync/whitelist/validation.js +60 -0
  76. package/dist/lib/graph/answer.js +171 -0
  77. package/dist/lib/graph/freshness.js +29 -0
  78. package/dist/lib/graph/library.js +19 -0
  79. package/dist/lib/guards.js +106 -0
  80. package/dist/lib/interactive.js +12 -0
  81. package/dist/lib/json_output.js +8 -0
  82. package/dist/lib/lanes/claudeAdapter.js +135 -0
  83. package/dist/lib/lanes/frames.js +73 -0
  84. package/dist/lib/lanes/supervisor.js +247 -0
  85. package/dist/lib/lanes/worktree.js +33 -0
  86. package/dist/lib/local_store/accepted_uploads.js +49 -0
  87. package/dist/lib/local_store/bindings.js +163 -0
  88. package/dist/lib/local_store/commit_origins.js +25 -0
  89. package/dist/lib/local_store/database.js +71 -0
  90. package/dist/lib/local_store/device_state.js +72 -0
  91. package/dist/lib/local_store/filebeat_state.js +133 -0
  92. package/dist/lib/local_store/handoff_applications.js +166 -0
  93. package/dist/lib/local_store/history_decisions.js +48 -0
  94. package/dist/lib/local_store/index.js +14 -0
  95. package/dist/lib/local_store/migrations.js +240 -0
  96. package/dist/lib/local_store/paths.js +24 -0
  97. package/dist/lib/local_store/pending_uploads.js +76 -0
  98. package/dist/lib/local_store/recovery_bundles.js +66 -0
  99. package/dist/lib/local_store/sync_vocabulary.js +33 -0
  100. package/dist/lib/local_store/workspace_events_cache.js +54 -0
  101. package/dist/lib/loopback.js +78 -0
  102. package/dist/lib/manifest.js +83 -0
  103. package/dist/lib/nustack_client.js +547 -0
  104. package/dist/lib/nustack_store.js +17 -0
  105. package/dist/lib/onboarding/attach_existing_git.js +65 -0
  106. package/dist/lib/onboarding/binding.js +91 -0
  107. package/dist/lib/onboarding/folder_state.js +99 -0
  108. package/dist/lib/onboarding/git_init.js +10 -0
  109. package/dist/lib/onboarding/handoff_apply.js +68 -0
  110. package/dist/lib/onboarding/init_fresh_folder.js +100 -0
  111. package/dist/lib/onboarding/manifest_writer.js +161 -0
  112. package/dist/lib/onboarding/web_first_attach.js +133 -0
  113. package/dist/lib/package_manager.js +23 -0
  114. package/dist/lib/review/client.js +66 -0
  115. package/dist/lib/schema_validator.js +24 -0
  116. package/dist/lib/secure_file.js +10 -0
  117. package/dist/lib/session_telemetry.js +263 -0
  118. package/dist/lib/sessions/client.js +48 -0
  119. package/dist/lib/sessions/outsideSessions.js +54 -0
  120. package/dist/lib/sse_framing.js +48 -0
  121. package/dist/lib/telemetry/config_renderer.js +175 -0
  122. package/dist/lib/telemetry/health_report.js +173 -0
  123. package/dist/lib/telemetry/provider_paths.js +38 -0
  124. package/dist/lib/telemetry/redaction_policy.js +90 -0
  125. package/dist/lib/telemetry/reload.js +84 -0
  126. package/dist/lib/workspace_context.js +121 -0
  127. package/dist/spec_schema.json +422 -0
  128. package/dist/wire/auth.js +1 -0
  129. package/dist/wire/sync-paths.js +25 -0
  130. package/dist/wire/telemetry.js +1 -0
  131. package/package.json +49 -0
@@ -0,0 +1,76 @@
1
+ function mapRow(row) {
2
+ return {
3
+ bindingId: row.binding_id,
4
+ commitId: row.commit_id,
5
+ manifestSha256: row.manifest_sha256 ?? null,
6
+ whitelistManifestSha256: row.whitelist_manifest_sha256 ?? null,
7
+ idempotencyKey: row.idempotency_key,
8
+ attempts: row.attempts,
9
+ nextRetryAtMs: row.next_retry_at_ms ?? null,
10
+ lastErrorCode: row.last_error_code ?? null,
11
+ lastErrorDetail: row.last_error_detail ?? null,
12
+ createdAtMs: row.created_at_ms,
13
+ updatedAtMs: row.updated_at_ms,
14
+ };
15
+ }
16
+ export function enqueueUpload(store, input) {
17
+ const now = Date.now();
18
+ store.db
19
+ .prepare(`INSERT INTO pending_uploads (
20
+ binding_id, commit_id, manifest_sha256, whitelist_manifest_sha256,
21
+ idempotency_key, attempts, next_retry_at_ms, created_at_ms, updated_at_ms
22
+ ) VALUES (
23
+ :binding_id, :commit_id, :manifest_sha256, :whitelist_manifest_sha256,
24
+ :idempotency_key, 0, :next_retry_at_ms, :created_at_ms, :updated_at_ms
25
+ )
26
+ ON CONFLICT(binding_id, commit_id) DO NOTHING`)
27
+ .run({
28
+ binding_id: input.bindingId,
29
+ commit_id: input.commitId,
30
+ manifest_sha256: input.manifestSha256 ?? null,
31
+ whitelist_manifest_sha256: input.whitelistManifestSha256 ?? null,
32
+ idempotency_key: input.idempotencyKey,
33
+ next_retry_at_ms: input.nextRetryAtMs ?? null,
34
+ created_at_ms: now,
35
+ updated_at_ms: now,
36
+ });
37
+ return getUpload(store, input.bindingId, input.commitId);
38
+ }
39
+ export function getUpload(store, bindingId, commitId) {
40
+ const row = store.db
41
+ .prepare("SELECT * FROM pending_uploads WHERE binding_id = :binding_id AND commit_id = :commit_id")
42
+ .get({ binding_id: bindingId, commit_id: commitId });
43
+ return row ? mapRow(row) : null;
44
+ }
45
+ export function nextDueUploads(store, nowMs) {
46
+ const rows = store.db
47
+ .prepare(`SELECT * FROM pending_uploads
48
+ WHERE next_retry_at_ms IS NULL OR next_retry_at_ms <= :now
49
+ ORDER BY created_at_ms ASC`)
50
+ .all({ now: nowMs });
51
+ return rows.map(mapRow);
52
+ }
53
+ export function recordUploadAttempt(store, bindingId, commitId, result = {}) {
54
+ store.db
55
+ .prepare(`UPDATE pending_uploads
56
+ SET attempts = attempts + 1,
57
+ next_retry_at_ms = :next_retry_at_ms,
58
+ last_error_code = :last_error_code,
59
+ last_error_detail = :last_error_detail,
60
+ updated_at_ms = :now
61
+ WHERE binding_id = :binding_id AND commit_id = :commit_id`)
62
+ .run({
63
+ next_retry_at_ms: result.nextRetryAtMs ?? null,
64
+ last_error_code: result.lastErrorCode ?? null,
65
+ last_error_detail: result.lastErrorDetail ?? null,
66
+ now: Date.now(),
67
+ binding_id: bindingId,
68
+ commit_id: commitId,
69
+ });
70
+ return getUpload(store, bindingId, commitId);
71
+ }
72
+ export function deleteUpload(store, bindingId, commitId) {
73
+ store.db
74
+ .prepare("DELETE FROM pending_uploads WHERE binding_id = :binding_id AND commit_id = :commit_id")
75
+ .run({ binding_id: bindingId, commit_id: commitId });
76
+ }
@@ -0,0 +1,66 @@
1
+ export const RECOVERY_BUNDLE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
2
+ function mapRow(row) {
3
+ return {
4
+ id: row.id,
5
+ bindingId: row.binding_id,
6
+ handoffId: row.handoff_id,
7
+ bundlePath: row.bundle_path,
8
+ manifestSha256: row.manifest_sha256,
9
+ state: row.state,
10
+ createdAtMs: row.created_at_ms,
11
+ expiresAtMs: row.expires_at_ms,
12
+ };
13
+ }
14
+ export function insertRecoveryBundle(store, input) {
15
+ const createdAtMs = input.createdAtMs ?? Date.now();
16
+ const expiresAtMs = input.expiresAtMs ?? createdAtMs + RECOVERY_BUNDLE_TTL_MS;
17
+ store.db
18
+ .prepare(`INSERT INTO local_recovery_bundles (
19
+ id, binding_id, handoff_id, bundle_path, manifest_sha256, state,
20
+ created_at_ms, expires_at_ms
21
+ ) VALUES (
22
+ :id, :binding_id, :handoff_id, :bundle_path, :manifest_sha256, :state,
23
+ :created_at_ms, :expires_at_ms
24
+ )`)
25
+ .run({
26
+ id: input.id,
27
+ binding_id: input.bindingId,
28
+ handoff_id: input.handoffId,
29
+ bundle_path: input.bundlePath,
30
+ manifest_sha256: input.manifestSha256,
31
+ state: input.state ?? "active",
32
+ created_at_ms: createdAtMs,
33
+ expires_at_ms: expiresAtMs,
34
+ });
35
+ return getRecoveryBundle(store, input.id);
36
+ }
37
+ export function getRecoveryBundle(store, id) {
38
+ const row = store.db
39
+ .prepare("SELECT * FROM local_recovery_bundles WHERE id = :id")
40
+ .get({ id });
41
+ return row ? mapRow(row) : null;
42
+ }
43
+ export function listRecoveryBundles(store, bindingId) {
44
+ const rows = (bindingId === undefined
45
+ ? store.db
46
+ .prepare("SELECT * FROM local_recovery_bundles ORDER BY created_at_ms ASC")
47
+ .all()
48
+ : store.db
49
+ .prepare("SELECT * FROM local_recovery_bundles WHERE binding_id = :binding_id ORDER BY created_at_ms ASC")
50
+ .all({ binding_id: bindingId }));
51
+ return rows.map(mapRow);
52
+ }
53
+ export function markRecoveryBundleState(store, id, state) {
54
+ store.db
55
+ .prepare("UPDATE local_recovery_bundles SET state = :state WHERE id = :id")
56
+ .run({ state, id });
57
+ return getRecoveryBundle(store, id);
58
+ }
59
+ export function listExpiredRecoveryBundles(store, nowMs) {
60
+ const rows = store.db
61
+ .prepare(`SELECT * FROM local_recovery_bundles
62
+ WHERE state = 'active' AND expires_at_ms <= :now
63
+ ORDER BY expires_at_ms ASC`)
64
+ .all({ now: nowMs });
65
+ return rows.map(mapRow);
66
+ }
@@ -0,0 +1,33 @@
1
+ export const INBOUND_APPLICATION_STATES = [
2
+ "received",
3
+ "evaluating",
4
+ "no_op",
5
+ "technical_wait",
6
+ "decision_required",
7
+ "approved",
8
+ "applying",
9
+ "committed",
10
+ "undo_requested",
11
+ "reverted",
12
+ "revert_conflict",
13
+ "superseded",
14
+ ];
15
+ export const BINDING_STATUSES = ["creating", "active", "paused", "failed", "removed"];
16
+ export const LIVE_BINDING_STATUSES = ["creating", "active", "paused"];
17
+ export const INBOUND_APPLICATION_TRANSITIONS = {
18
+ received: ["evaluating", "superseded"],
19
+ evaluating: ["no_op", "technical_wait", "decision_required", "applying", "superseded"],
20
+ no_op: [],
21
+ technical_wait: ["evaluating"],
22
+ decision_required: ["approved", "superseded"],
23
+ approved: ["applying"],
24
+ applying: ["committed"],
25
+ committed: ["undo_requested"],
26
+ undo_requested: ["reverted", "revert_conflict"],
27
+ reverted: [],
28
+ revert_conflict: [],
29
+ superseded: [],
30
+ };
31
+ export function isLegalApplicationTransition(from, to) {
32
+ return INBOUND_APPLICATION_TRANSITIONS[from].includes(to);
33
+ }
@@ -0,0 +1,54 @@
1
+ export const DEFAULT_WORKSPACE_EVENT_KEEP_COUNT = 500;
2
+ function mapRow(row) {
3
+ return {
4
+ bindingId: row.binding_id,
5
+ workspaceSequence: row.workspace_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 cacheWorkspaceEvents(store, bindingId, events) {
13
+ const now = Date.now();
14
+ const insert = store.db.prepare(`INSERT INTO cached_workspace_events (
15
+ binding_id, workspace_sequence, event_type, payload_json, applied_at_ms, created_at_ms
16
+ ) VALUES (
17
+ :binding_id, :workspace_sequence, :event_type, :payload_json, :applied_at_ms, :created_at_ms
18
+ )
19
+ ON CONFLICT(binding_id, workspace_sequence) DO NOTHING`);
20
+ for (const event of events) {
21
+ insert.run({
22
+ binding_id: bindingId,
23
+ workspace_sequence: event.workspaceSequence,
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(workspace_sequence) AS latest FROM cached_workspace_events WHERE binding_id = :binding_id")
34
+ .get({ binding_id: bindingId });
35
+ return row?.latest ?? null;
36
+ }
37
+ export function listCachedWorkspaceEvents(store, bindingId) {
38
+ const rows = store.db
39
+ .prepare("SELECT * FROM cached_workspace_events WHERE binding_id = :binding_id ORDER BY workspace_sequence ASC")
40
+ .all({ binding_id: bindingId });
41
+ return rows.map(mapRow);
42
+ }
43
+ export function pruneWorkspaceEvents(store, bindingId, keepCount = DEFAULT_WORKSPACE_EVENT_KEEP_COUNT) {
44
+ store.db
45
+ .prepare(`DELETE FROM cached_workspace_events
46
+ WHERE binding_id = :binding_id
47
+ AND workspace_sequence NOT IN (
48
+ SELECT workspace_sequence FROM cached_workspace_events
49
+ WHERE binding_id = :binding_id
50
+ ORDER BY workspace_sequence DESC
51
+ LIMIT :keep
52
+ )`)
53
+ .run({ binding_id: bindingId, keep: keepCount });
54
+ }
@@ -0,0 +1,78 @@
1
+ import { createServer } from "node:http";
2
+ export const CALLBACK_PATH = "/callback";
3
+ const escapeHtml = (value) => value.replace(/[&<>"']/g, (ch) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[ch] ?? ch);
4
+ function page(title, body) {
5
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${escapeHtml(title)}</title>
6
+ <style>body{font-family:system-ui,sans-serif;margin:0;display:grid;place-items:center;min-height:100vh;background:#0b0d12;color:#e8eaf0}main{max-width:32rem;padding:2rem;text-align:center}h1{font-size:1.4rem;margin:0 0 .5rem}p{color:#9aa3b2;margin:0}</style>
7
+ </head><body><main><h1>${escapeHtml(title)}</h1><p>${escapeHtml(body)}</p></main></body></html>`;
8
+ }
9
+ export async function openLoopbackListener(expectedState) {
10
+ let settle = null;
11
+ const callback = new Promise((resolve, reject) => {
12
+ settle = { resolve, reject };
13
+ });
14
+ callback.catch(() => undefined);
15
+ const server = createServer((request, response) => {
16
+ const url = new URL(request.url ?? "/", "http://127.0.0.1");
17
+ if (url.pathname !== CALLBACK_PATH) {
18
+ response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
19
+ response.end("Not found");
20
+ return;
21
+ }
22
+ if (request.method !== "GET") {
23
+ response.writeHead(405, { "Content-Type": "text/plain; charset=utf-8", Allow: "GET" });
24
+ response.end("Method not allowed");
25
+ return;
26
+ }
27
+ const state = url.searchParams.get("state");
28
+ if (state !== expectedState) {
29
+ response.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
30
+ response.end(page("This sign-in did not start here", "Go back to the terminal and run `nustack login` again."));
31
+ return;
32
+ }
33
+ const error = url.searchParams.get("error");
34
+ if (error) {
35
+ response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
36
+ response.end(page("Sign-in did not complete", url.searchParams.get("error_description") ?? error));
37
+ settle?.reject(new Error(`The auth service refused the sign-in (${error}).`));
38
+ settle = null;
39
+ void closeServer(server);
40
+ return;
41
+ }
42
+ const code = url.searchParams.get("code");
43
+ if (!code) {
44
+ response.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
45
+ response.end(page("Sign-in did not complete", "The callback carried no code. Run `nustack login` again."));
46
+ return;
47
+ }
48
+ response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
49
+ response.end(page("Signed in", "You can close this tab and return to the terminal."));
50
+ settle?.resolve({ code, iss: url.searchParams.get("iss") });
51
+ settle = null;
52
+ void closeServer(server);
53
+ });
54
+ await new Promise((resolve, reject) => {
55
+ server.once("error", reject);
56
+ server.listen(0, "127.0.0.1", () => {
57
+ server.off("error", reject);
58
+ resolve();
59
+ });
60
+ });
61
+ const { port } = server.address();
62
+ return {
63
+ redirectUri: `http://127.0.0.1:${port}${CALLBACK_PATH}`,
64
+ port,
65
+ callback,
66
+ close: async () => {
67
+ settle?.reject(new Error("The sign-in was abandoned before the browser answered."));
68
+ settle = null;
69
+ await closeServer(server);
70
+ },
71
+ };
72
+ }
73
+ async function closeServer(server) {
74
+ await new Promise((resolve) => {
75
+ server.close(() => resolve());
76
+ server.closeAllConnections?.();
77
+ });
78
+ }
@@ -0,0 +1,83 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { existsSync } from "node:fs";
3
+ import path from "node:path";
4
+ const MANIFEST_FILE = "nustack.service.json";
5
+ export function manifestPath(cwd) {
6
+ return path.join(cwd, MANIFEST_FILE);
7
+ }
8
+ export function hasManifest(cwd) {
9
+ return existsSync(manifestPath(cwd));
10
+ }
11
+ export async function readConsumesEdges(cwd) {
12
+ const filePath = manifestPath(cwd);
13
+ if (!existsSync(filePath))
14
+ return [];
15
+ let manifest;
16
+ try {
17
+ manifest = JSON.parse(await readFile(filePath, "utf8"));
18
+ }
19
+ catch {
20
+ throw new Error(`${MANIFEST_FILE} is not valid JSON`);
21
+ }
22
+ if (!manifest || typeof manifest !== "object")
23
+ return [];
24
+ const services = manifest.services;
25
+ if (!Array.isArray(services))
26
+ return [];
27
+ const seen = new Set();
28
+ const edges = [];
29
+ for (const entry of services) {
30
+ const consumes = entry?.consumes;
31
+ if (!Array.isArray(consumes))
32
+ continue;
33
+ for (const raw of consumes) {
34
+ const edge = raw;
35
+ const service = typeof edge?.service === "string" ? edge.service.trim() : "";
36
+ if (!service || seen.has(service))
37
+ continue;
38
+ seen.add(service);
39
+ edges.push({
40
+ service,
41
+ via: (edge?.via ?? "http"),
42
+ ...(typeof edge?.endpoint === "string" ? { endpoint: edge.endpoint } : {}),
43
+ });
44
+ }
45
+ }
46
+ return edges;
47
+ }
48
+ export async function scaffoldManifest(cwd, repoName) {
49
+ const manifest = {
50
+ spec: "nustack.dev/v1",
51
+ name: repoName,
52
+ description: `TODO: describe ${repoName}`,
53
+ repository: `https://github.com/TODO/${repoName}`,
54
+ owner: "TODO",
55
+ services: [
56
+ {
57
+ name: repoName,
58
+ kind: "application",
59
+ type: "app",
60
+ version: "0.0.0",
61
+ status: "experimental",
62
+ description: `TODO: describe the ${repoName} application`,
63
+ application: { ui: "spa" },
64
+ consumes: [],
65
+ },
66
+ ],
67
+ };
68
+ await writeFile(manifestPath(cwd), JSON.stringify(manifest, null, 4) + "\n", "utf8");
69
+ }
70
+ export async function appendConsumesEdge(cwd, edge) {
71
+ const filePath = manifestPath(cwd);
72
+ const manifest = JSON.parse(await readFile(filePath, "utf8"));
73
+ const serviceEntry = manifest.services?.[0];
74
+ if (!serviceEntry)
75
+ throw new Error(`${MANIFEST_FILE} has no services[] entry`);
76
+ serviceEntry.consumes ??= [];
77
+ const isAlreadyDeclared = serviceEntry.consumes.some((existing) => existing.service === edge.service);
78
+ if (isAlreadyDeclared)
79
+ return false;
80
+ serviceEntry.consumes.push(edge);
81
+ await writeFile(filePath, JSON.stringify(manifest, null, 4) + "\n", "utf8");
82
+ return true;
83
+ }