@nurix/nustack 0.10.0-dev.9 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/commands/bootstrap.js +4 -4
- package/dist/commands/docs.js +13 -13
- package/dist/commands/doctor.js +51 -17
- package/dist/commands/handoff.js +8 -8
- package/dist/commands/init.js +43 -43
- package/dist/commands/login.js +19 -6
- package/dist/commands/logout.js +14 -6
- package/dist/commands/logscope.js +55 -0
- 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 +38 -15
- package/dist/commands/search.js +7 -7
- package/dist/commands/sessions.js +156 -5
- package/dist/commands/sync.js +10 -10
- package/dist/commands/token.js +102 -0
- package/dist/commands/toolkit.js +19 -19
- package/dist/index.js +88 -36
- package/dist/lib/auth_grant.js +32 -0
- package/dist/lib/auth_recovery.js +3 -1
- package/dist/lib/credential_store.js +106 -0
- package/dist/lib/device_identity.js +6 -6
- package/dist/lib/device_keychain.js +16 -37
- 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/libsecret_store.js +47 -0
- 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/logs/answer.js +99 -0
- package/dist/lib/logs/library.js +19 -0
- package/dist/lib/logs/lifecycle.js +159 -0
- package/dist/lib/nustack_client.js +88 -46
- 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/{lanes → session-engine}/claudeAdapter.js +3 -3
- package/dist/lib/{lanes → session-engine}/frames.js +36 -27
- package/dist/lib/session-engine/prune.js +233 -0
- package/dist/lib/session-engine/registry.js +83 -0
- package/dist/lib/session-engine/supervisor.js +321 -0
- package/dist/lib/session-engine/worktree.js +89 -0
- package/dist/lib/session_telemetry.js +2 -2
- package/dist/lib/sessions/client.js +8 -8
- package/dist/lib/sessions/outsideSessions.js +6 -6
- package/dist/lib/telemetry/config_renderer.js +3 -3
- package/dist/lib/telemetry/health_report.js +1 -1
- package/package.json +5 -4
- package/dist/commands/lane.js +0 -12
- package/dist/lib/lanes/supervisor.js +0 -247
- package/dist/lib/lanes/worktree.js +0 -33
- package/dist/lib/local_store/workspace_events_cache.js +0 -54
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { printJsonSuccess } from "../json_output.js";
|
|
5
|
+
import { loadLogscope } from "./library.js";
|
|
6
|
+
export class LogsNoRepository extends Error {
|
|
7
|
+
code = "LOGS_NO_REPOSITORY";
|
|
8
|
+
constructor(where) {
|
|
9
|
+
super(`not a git repository: ${where} — pass --root <path> or run inside a checkout`);
|
|
10
|
+
this.name = "LogsNoRepository";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export class LogsUnavailable extends Error {
|
|
14
|
+
code = "LOGS_UNAVAILABLE";
|
|
15
|
+
constructor(reason) {
|
|
16
|
+
super(reason);
|
|
17
|
+
this.name = "LogsUnavailable";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export class LogsRefused extends Error {
|
|
21
|
+
code = "LOGS_REFUSED";
|
|
22
|
+
constructor(reason) {
|
|
23
|
+
super(reason);
|
|
24
|
+
this.name = "LogsRefused";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export class LogsNotFound extends Error {
|
|
28
|
+
code = "LOGS_NOT_FOUND";
|
|
29
|
+
constructor(reason) {
|
|
30
|
+
super(reason);
|
|
31
|
+
this.name = "LogsNotFound";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function resolveRoot(root, cwd) {
|
|
35
|
+
if (root !== undefined) {
|
|
36
|
+
const path = resolve(cwd, root);
|
|
37
|
+
if (!existsSync(path))
|
|
38
|
+
throw new LogsNoRepository(root);
|
|
39
|
+
return path;
|
|
40
|
+
}
|
|
41
|
+
return gitRoot(cwd);
|
|
42
|
+
}
|
|
43
|
+
function gitRoot(from) {
|
|
44
|
+
try {
|
|
45
|
+
return execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd: from, stdio: ["ignore", "pipe", "ignore"] })
|
|
46
|
+
.toString("utf8")
|
|
47
|
+
.trim();
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
throw new LogsNoRepository(from);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export const progress = (line) => {
|
|
54
|
+
process.stderr.write(`${line}\n`);
|
|
55
|
+
};
|
|
56
|
+
export async function answerLogscope(fn, argv, options, cwd) {
|
|
57
|
+
const library = await loadLogscope();
|
|
58
|
+
const known = library.findFn(fn);
|
|
59
|
+
if (known === undefined) {
|
|
60
|
+
throw new LogsUnavailable(`unknown logscope function '${fn}' — run \`nustack logscope functions\` for the list`);
|
|
61
|
+
}
|
|
62
|
+
const args = library.parseArgs(known, argv);
|
|
63
|
+
if (known.args["root"] !== undefined && args["root"] === undefined) {
|
|
64
|
+
args["root"] = resolveRoot(options.root, cwd);
|
|
65
|
+
}
|
|
66
|
+
if (args["limit"] === undefined && options.limit !== undefined)
|
|
67
|
+
args["limit"] = Number(options.limit);
|
|
68
|
+
const limit = args["limit"];
|
|
69
|
+
const budget = options.budget !== undefined ? Number(options.budget) : limit !== undefined ? 0 : library.DEFAULT_BUDGET;
|
|
70
|
+
const result = library.dispatch({ fn: known.name, args, corpus: options.corpus, budget });
|
|
71
|
+
if (result.answer.unavailable !== undefined)
|
|
72
|
+
throw new LogsUnavailable(result.answer.unavailable);
|
|
73
|
+
if (options.json === true) {
|
|
74
|
+
printJsonSuccess(`logscope ${known.name}`, result.answer);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
console.log(result.rendered);
|
|
78
|
+
}
|
|
79
|
+
export async function listLogscopeFunctions(options) {
|
|
80
|
+
const library = await loadLogscope();
|
|
81
|
+
const budget = options.budget !== undefined ? Number(options.budget) : 0;
|
|
82
|
+
const result = library.dispatch({ fn: "functions", args: {}, budget });
|
|
83
|
+
if (options.json === true) {
|
|
84
|
+
printJsonSuccess("logscope functions", result.answer);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
console.log(result.rendered);
|
|
88
|
+
console.log([
|
|
89
|
+
"",
|
|
90
|
+
"lifecycle — `nustack logscope <verb>`",
|
|
91
|
+
" allow <targets...> grant a log source; nothing is read before this",
|
|
92
|
+
" allowed the grants covering this project",
|
|
93
|
+
" revoke <targets...> withdraw grants",
|
|
94
|
+
" index <sources...> ingest or refresh a corpus from granted sources",
|
|
95
|
+
" corpora every corpus indexed on this machine",
|
|
96
|
+
" prune [--dry-run] drop every corpus past its retention",
|
|
97
|
+
" forget <id> drop one corpus and its store",
|
|
98
|
+
].join("\n"));
|
|
99
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export class LogsLibraryMissing extends Error {
|
|
2
|
+
code = "LOGS_LIBRARY_MISSING";
|
|
3
|
+
constructor() {
|
|
4
|
+
super("the log-tracking library is not installed with this nustack — install it beside the CLI (`npm i -g @nurix/logscope`) or run nustack from a checkout of the monorepo.");
|
|
5
|
+
this.name = "LogsLibraryMissing";
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
let cached = null;
|
|
9
|
+
export async function loadLogscope() {
|
|
10
|
+
if (cached !== null)
|
|
11
|
+
return cached;
|
|
12
|
+
try {
|
|
13
|
+
cached = (await import("@nurix/logscope"));
|
|
14
|
+
return cached;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
throw new LogsLibraryMissing();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { printJsonSuccess } from "../json_output.js";
|
|
2
|
+
import { loadLogscope } from "./library.js";
|
|
3
|
+
import { LogsNotFound, LogsRefused, progress, resolveRoot } from "./answer.js";
|
|
4
|
+
function reportPruneToStderr(library) {
|
|
5
|
+
for (const record of library.prune()) {
|
|
6
|
+
progress(`pruned ${record.id} (retention ${library.retentionOf(record)} days, indexed ${record.indexedAt ?? "unknown"})`);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export async function indexLogs(targets, options, cwd) {
|
|
10
|
+
const library = await loadLogscope();
|
|
11
|
+
const root = resolveRoot(options.root, cwd);
|
|
12
|
+
const retentionDays = Number(options.retain ?? String(library.DEFAULT_RETENTION_DAYS));
|
|
13
|
+
if (!Number.isInteger(retentionDays) || retentionDays < 0) {
|
|
14
|
+
throw new LogsRefused("--retain takes a non-negative whole number of days");
|
|
15
|
+
}
|
|
16
|
+
reportPruneToStderr(library);
|
|
17
|
+
const outcome = await library.indexCorpus({
|
|
18
|
+
targets,
|
|
19
|
+
root,
|
|
20
|
+
since: options.since,
|
|
21
|
+
until: options.until,
|
|
22
|
+
revision: options.revision,
|
|
23
|
+
redact: options.redact !== false,
|
|
24
|
+
keepExamples: options.examples === true,
|
|
25
|
+
retentionDays,
|
|
26
|
+
});
|
|
27
|
+
if (!outcome.ok)
|
|
28
|
+
throw new LogsRefused(outcome.message);
|
|
29
|
+
if (options.json === true) {
|
|
30
|
+
printJsonSuccess("logscope index", {
|
|
31
|
+
id: outcome.id,
|
|
32
|
+
sources: outcome.sources,
|
|
33
|
+
events: outcome.events,
|
|
34
|
+
templates: outcome.templates,
|
|
35
|
+
ratio: outcome.ratio,
|
|
36
|
+
resumes: outcome.resumes,
|
|
37
|
+
retentionDays: outcome.retentionDays,
|
|
38
|
+
hasExamples: outcome.hasExamples,
|
|
39
|
+
expiresAt: outcome.expiresAt ?? null,
|
|
40
|
+
warning: outcome.warning ?? null,
|
|
41
|
+
});
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
console.log([
|
|
45
|
+
`indexed ${outcome.id}`,
|
|
46
|
+
` ${outcome.events} events -> ${outcome.templates} templates (ratio ${outcome.ratio.toFixed(4)})`,
|
|
47
|
+
` sources ${Object.entries(outcome.resumes).map(([path, resume]) => `${path} [${resume}]`).join(", ")}`,
|
|
48
|
+
` retention ${outcome.retentionDays === 0 ? "off (kept until forgotten)" : `${outcome.retentionDays} days (expires ${outcome.expiresAt ?? "unknown"})`}`,
|
|
49
|
+
` examples ${outcome.hasExamples ? "kept" : "not kept — drill and trace <id> cannot answer on this corpus"}`,
|
|
50
|
+
...(outcome.warning === undefined ? [] : [` WARNING: ${outcome.warning}`]),
|
|
51
|
+
].join("\n"));
|
|
52
|
+
}
|
|
53
|
+
export async function listCorpora(options) {
|
|
54
|
+
const library = await loadLogscope();
|
|
55
|
+
reportPruneToStderr(library);
|
|
56
|
+
const corpora = library.readRegistry().map((record) => ({
|
|
57
|
+
id: record.id,
|
|
58
|
+
sources: record.sources,
|
|
59
|
+
events: record.events ?? 0,
|
|
60
|
+
templates: record.templates ?? 0,
|
|
61
|
+
indexedAt: record.indexedAt ?? null,
|
|
62
|
+
retentionDays: library.retentionOf(record),
|
|
63
|
+
expiresAt: library.expiresAt(record) ?? null,
|
|
64
|
+
hasExamples: record.hasExamples === true,
|
|
65
|
+
}));
|
|
66
|
+
if (options.json === true) {
|
|
67
|
+
printJsonSuccess("logscope corpora", { corpora });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (corpora.length === 0) {
|
|
71
|
+
console.log("no corpus has been indexed on this machine — run: nustack logscope index <sources...>");
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
console.log(corpora
|
|
75
|
+
.map((corpus) => `${corpus.id} ${corpus.events} events, ${corpus.templates} templates (${corpus.sources.length} sources) ` +
|
|
76
|
+
(corpus.retentionDays === 0
|
|
77
|
+
? "retention off"
|
|
78
|
+
: `retention ${corpus.retentionDays} days, expires ${corpus.expiresAt ?? "unknown"}`))
|
|
79
|
+
.join("\n"));
|
|
80
|
+
}
|
|
81
|
+
export async function allowSources(targets, options, cwd) {
|
|
82
|
+
const library = await loadLogscope();
|
|
83
|
+
const root = resolveRoot(options.root, cwd);
|
|
84
|
+
const allowed = library.allow(root, targets, options.note).map((grant) => grant.path);
|
|
85
|
+
if (options.json === true) {
|
|
86
|
+
printJsonSuccess("logscope allow", { root, allowed });
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
console.log(allowed.map((path) => `allowed ${path}`).join("\n"));
|
|
90
|
+
}
|
|
91
|
+
export async function listAllowed(options, cwd) {
|
|
92
|
+
const library = await loadLogscope();
|
|
93
|
+
const root = resolveRoot(options.root, cwd);
|
|
94
|
+
const grants = library.allowedFor(root);
|
|
95
|
+
if (options.json === true) {
|
|
96
|
+
printJsonSuccess("logscope allowed", { root, grants });
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (grants.length === 0) {
|
|
100
|
+
console.log(`no source is allowed for ${root} — run: nustack logscope allow <path...>`);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
console.log(grants
|
|
104
|
+
.map((grant) => `${grant.path} granted ${grant.grantedAt}${grant.note === undefined ? "" : ` ${grant.note}`}`)
|
|
105
|
+
.join("\n"));
|
|
106
|
+
}
|
|
107
|
+
export async function revokeSources(targets, options, cwd) {
|
|
108
|
+
const library = await loadLogscope();
|
|
109
|
+
const root = resolveRoot(options.root, cwd);
|
|
110
|
+
const revoked = library.revoke(root, targets);
|
|
111
|
+
if (revoked === 0)
|
|
112
|
+
throw new LogsNotFound(`no grant under ${root} names ${targets.join(", ")}`);
|
|
113
|
+
if (options.json === true) {
|
|
114
|
+
printJsonSuccess("logscope revoke", { root, revoked });
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
console.log(`revoked ${revoked} grant(s)`);
|
|
118
|
+
}
|
|
119
|
+
export async function pruneCorpora(options, cwd) {
|
|
120
|
+
const library = await loadLogscope();
|
|
121
|
+
resolveRoot(options.root, cwd);
|
|
122
|
+
const dryRun = options.dryRun === true;
|
|
123
|
+
const now = new Date();
|
|
124
|
+
const candidates = dryRun
|
|
125
|
+
? library.readRegistry().filter((record) => {
|
|
126
|
+
const at = library.expiresAt(record);
|
|
127
|
+
return at !== null && at !== undefined && new Date(at).getTime() <= now.getTime();
|
|
128
|
+
})
|
|
129
|
+
: library.prune();
|
|
130
|
+
const pruned = candidates.map((record) => ({
|
|
131
|
+
id: record.id,
|
|
132
|
+
retentionDays: library.retentionOf(record),
|
|
133
|
+
indexedAt: record.indexedAt ?? null,
|
|
134
|
+
}));
|
|
135
|
+
if (options.json === true) {
|
|
136
|
+
printJsonSuccess("logscope prune", { dryRun, pruned });
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (pruned.length === 0) {
|
|
140
|
+
console.log("nothing to prune");
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
console.log(pruned
|
|
144
|
+
.map((record) => `${dryRun ? "would prune" : "pruned"} ${record.id} (retention ${record.retentionDays} days, indexed ${record.indexedAt ?? "unknown"})`)
|
|
145
|
+
.join("\n"));
|
|
146
|
+
}
|
|
147
|
+
export async function forgetCorpusCommand(ref, options) {
|
|
148
|
+
const library = await loadLogscope();
|
|
149
|
+
if (ref === undefined)
|
|
150
|
+
throw new LogsNotFound("name the corpus to forget — run: nustack logscope corpora");
|
|
151
|
+
const forgot = library.forgetCorpus(ref);
|
|
152
|
+
if (forgot === undefined)
|
|
153
|
+
throw new LogsNotFound(`no corpus matches ${ref}`);
|
|
154
|
+
if (options.json === true) {
|
|
155
|
+
printJsonSuccess("logscope forget", { forgot });
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
console.log(`forgot ${forgot}`);
|
|
159
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { assertSafeNuStackUrl, assertSafeOpaqueId } from "./guards.js";
|
|
2
2
|
import { createSseParser } from "./sse_framing.js";
|
|
3
|
-
import { DEVICE_HEADER, knownDeviceId, refreshAccessTokenAfterRefusal, userAgent } from "./auth_grant.js";
|
|
3
|
+
import { DEVICE_HEADER, envToken, knownDeviceId, refreshAccessTokenAfterRefusal, TokenRefusedError, userAgent } from "./auth_grant.js";
|
|
4
4
|
import { getOrCreateDeviceId } from "./device_identity.js";
|
|
5
5
|
const DEFAULT_NUSTACK_URL = "https://apps.nustack.ai";
|
|
6
6
|
const REQUEST_TIMEOUT_MS = 10_000;
|
|
@@ -12,6 +12,8 @@ async function authorizedFetch(input, init = {}) {
|
|
|
12
12
|
const bearer = headers.get("Authorization")?.replace(/^Bearer\s+/i, "");
|
|
13
13
|
if (response.status !== 401 || !bearer || isStreamBody(init.body))
|
|
14
14
|
return response;
|
|
15
|
+
if (envToken())
|
|
16
|
+
throw new TokenRefusedError();
|
|
15
17
|
const fresh = await refreshAccessTokenAfterRefusal(bearer).catch(() => null);
|
|
16
18
|
if (!fresh)
|
|
17
19
|
return response;
|
|
@@ -60,7 +62,7 @@ export async function exchangeServiceCredential(baseUrl, accessToken, params) {
|
|
|
60
62
|
const response = await authorizedFetch(`${baseUrl}/api/v1/keys/exchange`, {
|
|
61
63
|
method: "POST",
|
|
62
64
|
headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
|
|
63
|
-
body: JSON.stringify({
|
|
65
|
+
body: JSON.stringify({ projectId: params.projectId, service: params.service }),
|
|
64
66
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
65
67
|
});
|
|
66
68
|
const payload = (await response.json().catch(() => null));
|
|
@@ -71,7 +73,7 @@ export async function exchangeServiceCredential(baseUrl, accessToken, params) {
|
|
|
71
73
|
}
|
|
72
74
|
export async function fetchServiceCredential(baseUrl, accessToken, params) {
|
|
73
75
|
const url = new URL(`${baseUrl}/api/v1/keys/credentials`);
|
|
74
|
-
url.searchParams.set("
|
|
76
|
+
url.searchParams.set("projectId", assertSafeOpaqueId("The Project id", params.projectId));
|
|
75
77
|
url.searchParams.set("service", assertSafeOpaqueId("The Service slug", params.service));
|
|
76
78
|
const response = await authorizedFetch(url, {
|
|
77
79
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
@@ -83,9 +85,9 @@ export async function fetchServiceCredential(baseUrl, accessToken, params) {
|
|
|
83
85
|
}
|
|
84
86
|
return narrowProjection(payload, params.service);
|
|
85
87
|
}
|
|
86
|
-
export async function
|
|
88
|
+
export async function listProjectIntegrations(baseUrl, accessToken, projectId) {
|
|
87
89
|
const url = new URL(`${baseUrl}/api/v1/keys/integrations`);
|
|
88
|
-
url.searchParams.set("
|
|
90
|
+
url.searchParams.set("projectId", assertSafeOpaqueId("The Project id", projectId));
|
|
89
91
|
const response = await authorizedFetch(url, {
|
|
90
92
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
91
93
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
@@ -101,7 +103,7 @@ export async function fetchBrokeredCredential(baseUrl, accessToken, params) {
|
|
|
101
103
|
const response = await authorizedFetch(`${baseUrl}/api/v1/keys/broker/${encodeURIComponent(service)}`, {
|
|
102
104
|
method: "POST",
|
|
103
105
|
headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
|
|
104
|
-
body: JSON.stringify({
|
|
106
|
+
body: JSON.stringify({ projectId: params.projectId }),
|
|
105
107
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
106
108
|
});
|
|
107
109
|
const payload = (await response.json().catch(() => null));
|
|
@@ -110,8 +112,8 @@ export async function fetchBrokeredCredential(baseUrl, accessToken, params) {
|
|
|
110
112
|
}
|
|
111
113
|
return { url: payload.url ?? null, key: payload.key, meta: payload.meta ?? null, grantId: payload.grantId };
|
|
112
114
|
}
|
|
113
|
-
export async function
|
|
114
|
-
const url = new URL(`${baseUrl}/public/v1/
|
|
115
|
+
export async function searchProjects(baseUrl, query, opts = {}) {
|
|
116
|
+
const url = new URL(`${baseUrl}/public/v1/projects`);
|
|
115
117
|
url.searchParams.set("q", query);
|
|
116
118
|
if (opts.category)
|
|
117
119
|
url.searchParams.set("category", opts.category);
|
|
@@ -120,7 +122,7 @@ export async function searchWorkspaces(baseUrl, query, opts = {}) {
|
|
|
120
122
|
const response = await authorizedFetch(url, { signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
|
|
121
123
|
const payload = (await response.json().catch(() => null));
|
|
122
124
|
if (!response.ok || !Array.isArray(payload?.data?.items)) {
|
|
123
|
-
throw new Error(errorMessage(payload, response.status, "
|
|
125
|
+
throw new Error(errorMessage(payload, response.status, "project search failed"));
|
|
124
126
|
}
|
|
125
127
|
return payload.data.items;
|
|
126
128
|
}
|
|
@@ -190,10 +192,10 @@ export async function acknowledgeBindingCursor(baseUrl, accessToken, params) {
|
|
|
190
192
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
191
193
|
});
|
|
192
194
|
const payload = (await response.json().catch(() => null));
|
|
193
|
-
if (!response.ok || typeof payload?.data?.
|
|
195
|
+
if (!response.ok || typeof payload?.data?.lastAppliedProjectSequence !== "number") {
|
|
194
196
|
throw new Error(errorMessage(payload, response.status, "cursor acknowledgement failed"));
|
|
195
197
|
}
|
|
196
|
-
return payload.data.
|
|
198
|
+
return payload.data.lastAppliedProjectSequence;
|
|
197
199
|
}
|
|
198
200
|
export async function fetchSyncWhitelist(baseUrl, accessToken, bindingId) {
|
|
199
201
|
const safeId = assertSafeOpaqueId("The binding id", bindingId);
|
|
@@ -282,6 +284,46 @@ export async function listOrganizations(baseUrl, accessToken) {
|
|
|
282
284
|
}
|
|
283
285
|
return payload.data.items;
|
|
284
286
|
}
|
|
287
|
+
export class PersonalTokenRequestError extends Error {
|
|
288
|
+
code;
|
|
289
|
+
constructor(code, message) {
|
|
290
|
+
super(message);
|
|
291
|
+
this.code = code;
|
|
292
|
+
this.name = "PersonalTokenRequestError";
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
async function readTokenEnvelope(response, label) {
|
|
296
|
+
const payload = (await response.json().catch(() => null));
|
|
297
|
+
if (!response.ok || payload?.data === undefined) {
|
|
298
|
+
throw new PersonalTokenRequestError(payload?.error?.code ?? "REQUEST_FAILED", errorMessage(payload, response.status, label));
|
|
299
|
+
}
|
|
300
|
+
return payload.data;
|
|
301
|
+
}
|
|
302
|
+
export async function listPersonalTokens(baseUrl, accessToken) {
|
|
303
|
+
const response = await authorizedFetch(`${baseUrl}/api/v2/tokens`, {
|
|
304
|
+
headers: authHeaders(accessToken),
|
|
305
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
306
|
+
});
|
|
307
|
+
const data = await readTokenEnvelope(response, "token list failed");
|
|
308
|
+
return Array.isArray(data.items) ? data.items : [];
|
|
309
|
+
}
|
|
310
|
+
export async function createPersonalToken(baseUrl, accessToken, params) {
|
|
311
|
+
const response = await authorizedFetch(`${baseUrl}/api/v2/tokens`, {
|
|
312
|
+
method: "POST",
|
|
313
|
+
headers: { ...authHeaders(accessToken), "Content-Type": "application/json" },
|
|
314
|
+
body: JSON.stringify(params),
|
|
315
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
316
|
+
});
|
|
317
|
+
return readTokenEnvelope(response, "token create failed");
|
|
318
|
+
}
|
|
319
|
+
export async function revokePersonalToken(baseUrl, accessToken, tokenId) {
|
|
320
|
+
const response = await authorizedFetch(`${baseUrl}/api/v2/tokens/${encodeURIComponent(assertSafeOpaqueId("The token id", tokenId))}`, {
|
|
321
|
+
method: "DELETE",
|
|
322
|
+
headers: authHeaders(accessToken),
|
|
323
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
324
|
+
});
|
|
325
|
+
return readTokenEnvelope(response, "token revoke failed");
|
|
326
|
+
}
|
|
285
327
|
export async function createOrganization(baseUrl, accessToken, params, idempotencyKey) {
|
|
286
328
|
const response = await authorizedFetch(`${baseUrl}/api/v2/organizations`, {
|
|
287
329
|
method: "POST",
|
|
@@ -295,31 +337,31 @@ export async function createOrganization(baseUrl, accessToken, params, idempoten
|
|
|
295
337
|
}
|
|
296
338
|
return payload.data;
|
|
297
339
|
}
|
|
298
|
-
export async function
|
|
340
|
+
export async function listProjects(baseUrl, accessToken, organizationId) {
|
|
299
341
|
const safeId = assertSafeOpaqueId("The Organization id", organizationId);
|
|
300
|
-
const response = await authorizedFetch(`${baseUrl}/api/v2/
|
|
342
|
+
const response = await authorizedFetch(`${baseUrl}/api/v2/projects`, {
|
|
301
343
|
headers: { ...authHeaders(accessToken), "X-NuStack-Organization": safeId },
|
|
302
344
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
303
345
|
});
|
|
304
346
|
const payload = (await response.json().catch(() => null));
|
|
305
347
|
if (!response.ok || !Array.isArray(payload?.data?.items)) {
|
|
306
|
-
throw new Error(errorMessage(payload, response.status, "
|
|
348
|
+
throw new Error(errorMessage(payload, response.status, "project list failed"));
|
|
307
349
|
}
|
|
308
350
|
return payload.data.items;
|
|
309
351
|
}
|
|
310
|
-
export async function
|
|
311
|
-
const safeId = assertSafeOpaqueId("The
|
|
312
|
-
const response = await authorizedFetch(`${baseUrl}/api/v2/
|
|
352
|
+
export async function getProject(baseUrl, accessToken, projectId) {
|
|
353
|
+
const safeId = assertSafeOpaqueId("The Project id", projectId);
|
|
354
|
+
const response = await authorizedFetch(`${baseUrl}/api/v2/projects/${encodeURIComponent(safeId)}`, {
|
|
313
355
|
headers: authHeaders(accessToken),
|
|
314
356
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
315
357
|
});
|
|
316
358
|
const payload = (await response.json().catch(() => null));
|
|
317
359
|
if (!response.ok || !payload?.data?.id) {
|
|
318
|
-
throw new Error(errorMessage(payload, response.status, "
|
|
360
|
+
throw new Error(errorMessage(payload, response.status, "project fetch failed"));
|
|
319
361
|
}
|
|
320
362
|
return payload.data;
|
|
321
363
|
}
|
|
322
|
-
export function
|
|
364
|
+
export function mintProjectId() {
|
|
323
365
|
const bytes = crypto.getRandomValues(new Uint8Array(16));
|
|
324
366
|
const ms = Date.now();
|
|
325
367
|
bytes[0] = Math.floor(ms / 2 ** 40) & 0xff;
|
|
@@ -360,9 +402,9 @@ async function* readSseFrames(body) {
|
|
|
360
402
|
for (const block of parser.flush())
|
|
361
403
|
yield toFrame(block);
|
|
362
404
|
}
|
|
363
|
-
export async function
|
|
405
|
+
export async function createProjectViaAsk(baseUrl, accessToken, organizationId, prompt, opts = {}) {
|
|
364
406
|
const safeOrganization = assertSafeOpaqueId("The Organization id", organizationId);
|
|
365
|
-
const
|
|
407
|
+
const projectId = opts.projectId ?? mintProjectId();
|
|
366
408
|
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
367
409
|
const response = await fetchImpl(`${baseUrl}/api/v2/ask`, {
|
|
368
410
|
method: "POST",
|
|
@@ -371,11 +413,11 @@ export async function createWorkspaceViaAsk(baseUrl, accessToken, organizationId
|
|
|
371
413
|
"Content-Type": "application/json",
|
|
372
414
|
"X-NuStack-Organization": safeOrganization,
|
|
373
415
|
},
|
|
374
|
-
body: JSON.stringify({ prompt,
|
|
416
|
+
body: JSON.stringify({ prompt, projectId }),
|
|
375
417
|
});
|
|
376
418
|
if (!response.ok || !response.body) {
|
|
377
419
|
const payload = (await response.json().catch(() => null));
|
|
378
|
-
throw new Error(errorMessage(payload, response.status, "
|
|
420
|
+
throw new Error(errorMessage(payload, response.status, "project create failed"));
|
|
379
421
|
}
|
|
380
422
|
let started = false;
|
|
381
423
|
let turnId = null;
|
|
@@ -384,7 +426,7 @@ export async function createWorkspaceViaAsk(baseUrl, accessToken, organizationId
|
|
|
384
426
|
if (frame.event === "start") {
|
|
385
427
|
started = true;
|
|
386
428
|
turnId = typeof frame.data.turnId === "string" ? frame.data.turnId : null;
|
|
387
|
-
opts.onPhase?.("
|
|
429
|
+
opts.onPhase?.("Project created — writing the first document");
|
|
388
430
|
}
|
|
389
431
|
else if (frame.event === "thinking" && typeof frame.data.message === "string") {
|
|
390
432
|
opts.onPhase?.(frame.data.message);
|
|
@@ -392,7 +434,7 @@ export async function createWorkspaceViaAsk(baseUrl, accessToken, organizationId
|
|
|
392
434
|
else if (frame.event === "error") {
|
|
393
435
|
const message = typeof frame.data.message === "string" ? frame.data.message : "generation failed";
|
|
394
436
|
if (!started)
|
|
395
|
-
throw new Error(`
|
|
437
|
+
throw new Error(`project create failed: ${message}`);
|
|
396
438
|
generationError = message;
|
|
397
439
|
break;
|
|
398
440
|
}
|
|
@@ -401,12 +443,12 @@ export async function createWorkspaceViaAsk(baseUrl, accessToken, organizationId
|
|
|
401
443
|
}
|
|
402
444
|
}
|
|
403
445
|
if (!started)
|
|
404
|
-
throw new Error("
|
|
405
|
-
return {
|
|
446
|
+
throw new Error("project create failed: the stream ended before the Project was reported");
|
|
447
|
+
return { projectId, turnId, generationError };
|
|
406
448
|
}
|
|
407
|
-
async function
|
|
408
|
-
const safeId = assertSafeOpaqueId("The
|
|
409
|
-
const response = await authorizedFetch(`${baseUrl}/api/v2/
|
|
449
|
+
async function postProjectPublication(baseUrl, accessToken, projectId, action, body, idempotencyKey) {
|
|
450
|
+
const safeId = assertSafeOpaqueId("The Project id", projectId);
|
|
451
|
+
const response = await authorizedFetch(`${baseUrl}/api/v2/projects/${encodeURIComponent(safeId)}/${action}`, {
|
|
410
452
|
method: "POST",
|
|
411
453
|
headers: mutationHeaders(accessToken, idempotencyKey),
|
|
412
454
|
body: JSON.stringify(body),
|
|
@@ -414,19 +456,19 @@ async function postWorkspacePublication(baseUrl, accessToken, workspaceId, actio
|
|
|
414
456
|
});
|
|
415
457
|
const payload = (await response.json().catch(() => null));
|
|
416
458
|
if (!response.ok || payload?.data === undefined) {
|
|
417
|
-
throw new Error(errorMessage(payload, response.status, `
|
|
459
|
+
throw new Error(errorMessage(payload, response.status, `project ${action} failed`));
|
|
418
460
|
}
|
|
419
461
|
return payload.data;
|
|
420
462
|
}
|
|
421
|
-
export async function
|
|
422
|
-
return
|
|
463
|
+
export async function publishProject(baseUrl, accessToken, projectId, profile, idempotencyKey) {
|
|
464
|
+
return postProjectPublication(baseUrl, accessToken, projectId, "publish", profile, idempotencyKey);
|
|
423
465
|
}
|
|
424
|
-
export async function
|
|
425
|
-
return
|
|
466
|
+
export async function unpublishProject(baseUrl, accessToken, projectId, idempotencyKey) {
|
|
467
|
+
return postProjectPublication(baseUrl, accessToken, projectId, "unpublish", {}, idempotencyKey);
|
|
426
468
|
}
|
|
427
|
-
export async function listDocuments(baseUrl, accessToken,
|
|
428
|
-
const safeId = assertSafeOpaqueId("The
|
|
429
|
-
const response = await authorizedFetch(`${baseUrl}/api/v2/documents?
|
|
469
|
+
export async function listDocuments(baseUrl, accessToken, projectId) {
|
|
470
|
+
const safeId = assertSafeOpaqueId("The Project id", projectId);
|
|
471
|
+
const response = await authorizedFetch(`${baseUrl}/api/v2/documents?projectId=${encodeURIComponent(safeId)}`, {
|
|
430
472
|
headers: authHeaders(accessToken),
|
|
431
473
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
432
474
|
});
|
|
@@ -448,9 +490,9 @@ export async function getDocument(baseUrl, accessToken, documentId) {
|
|
|
448
490
|
}
|
|
449
491
|
return payload.data;
|
|
450
492
|
}
|
|
451
|
-
export async function listHandoffs(baseUrl, accessToken,
|
|
452
|
-
const safeId = assertSafeOpaqueId("The
|
|
453
|
-
const response = await authorizedFetch(`${baseUrl}/api/v2/
|
|
493
|
+
export async function listHandoffs(baseUrl, accessToken, projectId) {
|
|
494
|
+
const safeId = assertSafeOpaqueId("The Project id", projectId);
|
|
495
|
+
const response = await authorizedFetch(`${baseUrl}/api/v2/projects/${encodeURIComponent(safeId)}/handoffs`, {
|
|
454
496
|
headers: authHeaders(accessToken),
|
|
455
497
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
456
498
|
});
|
|
@@ -472,13 +514,13 @@ export async function getHandoff(baseUrl, accessToken, handoffId) {
|
|
|
472
514
|
}
|
|
473
515
|
return payload.data;
|
|
474
516
|
}
|
|
475
|
-
export async function latestHandoff(baseUrl, accessToken,
|
|
476
|
-
const handoffs = await listHandoffs(baseUrl, accessToken,
|
|
517
|
+
export async function latestHandoff(baseUrl, accessToken, projectId) {
|
|
518
|
+
const handoffs = await listHandoffs(baseUrl, accessToken, projectId);
|
|
477
519
|
return handoffs.length > 0 ? handoffs[0] : null;
|
|
478
520
|
}
|
|
479
|
-
export async function createBinding(baseUrl, accessToken,
|
|
480
|
-
const safeId = assertSafeOpaqueId("The
|
|
481
|
-
const response = await authorizedFetch(`${baseUrl}/api/v2/
|
|
521
|
+
export async function createBinding(baseUrl, accessToken, projectId, input, idempotencyKey) {
|
|
522
|
+
const safeId = assertSafeOpaqueId("The Project id", projectId);
|
|
523
|
+
const response = await authorizedFetch(`${baseUrl}/api/v2/projects/${encodeURIComponent(safeId)}/bindings`, {
|
|
482
524
|
method: "POST",
|
|
483
525
|
headers: mutationHeaders(accessToken, idempotencyKey),
|
|
484
526
|
body: JSON.stringify({
|
|
@@ -7,24 +7,24 @@ import { runRepositoryOnboarding, } from "./binding.js";
|
|
|
7
7
|
export async function attachExistingGit(ctx, options) {
|
|
8
8
|
const state = classifyFolder(ctx.cwd);
|
|
9
9
|
if (state.class !== "git_worktree" || !state.repository || !state.git) {
|
|
10
|
-
return { ok: false, code: "NOT_A_WORKTREE", message: "`nustack
|
|
10
|
+
return { ok: false, code: "NOT_A_WORKTREE", message: "`nustack project attach` needs an existing Git worktree — run `nustack init` to establish one." };
|
|
11
11
|
}
|
|
12
12
|
const rejection = attachRejection(state.git);
|
|
13
13
|
if (rejection) {
|
|
14
14
|
return { ok: false, code: rejection.toUpperCase(), message: rejectionMessage(rejection) };
|
|
15
15
|
}
|
|
16
|
-
if (state.git.
|
|
16
|
+
if (state.git.markerProjectId && state.git.markerProjectId !== options.projectId && !options.confirmRebind) {
|
|
17
17
|
return {
|
|
18
18
|
ok: false,
|
|
19
19
|
code: "REBIND_CONFIRMATION_REQUIRED",
|
|
20
|
-
message: `This repository is bound to
|
|
20
|
+
message: `This repository is bound to Project ${state.git.markerProjectId}. Re-run with confirmation to rebind it to ${options.projectId}.`,
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
23
|
const boundBranch = readSymbolicBranch(state.repository);
|
|
24
24
|
if (!boundBranch) {
|
|
25
25
|
return { ok: false, code: "DETACHED_HEAD", message: "HEAD is detached — check out a branch before attaching." };
|
|
26
26
|
}
|
|
27
|
-
const profile = await fetchToolkitProfile(ctx.baseUrl, ctx.accessToken, options.
|
|
27
|
+
const profile = await fetchToolkitProfile(ctx.baseUrl, ctx.accessToken, options.projectId);
|
|
28
28
|
const baselineCommitId = readHeadCommit(state.repository);
|
|
29
29
|
const result = await runRepositoryOnboarding({
|
|
30
30
|
store: ctx.store,
|
|
@@ -32,7 +32,7 @@ export async function attachExistingGit(ctx, options) {
|
|
|
32
32
|
accessToken: ctx.accessToken,
|
|
33
33
|
deviceRef: ctx.deviceRef,
|
|
34
34
|
flow: "existing_git",
|
|
35
|
-
|
|
35
|
+
projectId: options.projectId,
|
|
36
36
|
organizationId: options.organizationId,
|
|
37
37
|
repository: state.repository,
|
|
38
38
|
boundBranch,
|
|
@@ -55,8 +55,8 @@ async function reconcileLatest(ctx, bindingId) {
|
|
|
55
55
|
}
|
|
56
56
|
function rejectionMessage(rejection) {
|
|
57
57
|
switch (rejection) {
|
|
58
|
-
case "
|
|
59
|
-
return "This repository is nested inside another NuStack
|
|
58
|
+
case "nested_project":
|
|
59
|
+
return "This repository is nested inside another NuStack Project — one Project has one logical repository root (F-014).";
|
|
60
60
|
case "operation_in_progress":
|
|
61
61
|
return "A merge, rebase, cherry-pick, or revert is in progress — finish or abort it, then re-attach.";
|
|
62
62
|
case "detached_head":
|