@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.
- 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 +3 -2
- 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
package/dist/commands/review.js
CHANGED
|
@@ -8,16 +8,30 @@ const EXIT_FOR = {
|
|
|
8
8
|
none: 1,
|
|
9
9
|
unreachable: 2,
|
|
10
10
|
};
|
|
11
|
+
const latestAnswered = (items) => items.find((item) => item.answer?.verdict === "signed" || item.answer?.verdict === "refused");
|
|
11
12
|
export function decisionStateOf(items) {
|
|
12
|
-
const latest = items
|
|
13
|
+
const latest = latestAnswered(items);
|
|
13
14
|
if (!latest)
|
|
14
15
|
return "none";
|
|
15
16
|
return latest.answer?.verdict === "signed" ? "signed" : "refused";
|
|
16
17
|
}
|
|
17
|
-
function
|
|
18
|
+
export function waivedKeysOf(items) {
|
|
19
|
+
const latest = latestAnswered(items);
|
|
20
|
+
if (latest?.answer?.verdict !== "signed")
|
|
21
|
+
return [];
|
|
22
|
+
const waivers = latest.answer.waivers;
|
|
23
|
+
if (!Array.isArray(waivers))
|
|
24
|
+
return [];
|
|
25
|
+
return waivers
|
|
26
|
+
.map((waiver) => waiver?.criterionKey)
|
|
27
|
+
.filter((key) => typeof key === "string" && key.length > 0);
|
|
28
|
+
}
|
|
29
|
+
function checkLine(state, commit, waived) {
|
|
18
30
|
switch (state) {
|
|
19
31
|
case "signed":
|
|
20
|
-
return
|
|
32
|
+
return waived.length
|
|
33
|
+
? `review check: ${commit} is signed off — ${waived.length} ${waived.length === 1 ? "criterion" : "criteria"} waived: ${waived.join(", ")}\n`
|
|
34
|
+
: `review check: ${commit} is signed off\n`;
|
|
21
35
|
case "refused":
|
|
22
36
|
return `review check: ${commit} was refused\n`;
|
|
23
37
|
case "none":
|
|
@@ -34,11 +48,11 @@ export async function runReviewCheck(options, globals) {
|
|
|
34
48
|
process.exitCode = 2;
|
|
35
49
|
return;
|
|
36
50
|
}
|
|
37
|
-
const report = (state) => {
|
|
51
|
+
const report = (state, waived = []) => {
|
|
38
52
|
if (json)
|
|
39
|
-
printJsonSuccess("review check", { commit, state, exitCode: EXIT_FOR[state] });
|
|
53
|
+
printJsonSuccess("review check", { commit, state, exitCode: EXIT_FOR[state], waived });
|
|
40
54
|
else
|
|
41
|
-
process.stdout.write(checkLine(state, commit));
|
|
55
|
+
process.stdout.write(checkLine(state, commit, waived));
|
|
42
56
|
process.exitCode = EXIT_FOR[state];
|
|
43
57
|
};
|
|
44
58
|
const baseUrl = resolveNuStackUrl(globals);
|
|
@@ -47,7 +61,7 @@ export async function runReviewCheck(options, globals) {
|
|
|
47
61
|
return report("unreachable");
|
|
48
62
|
try {
|
|
49
63
|
const items = await makeReviewClient(baseUrl, deviceAuth.accessToken).readDecisions(commit, options.repository?.trim() || null);
|
|
50
|
-
report(decisionStateOf(items));
|
|
64
|
+
report(decisionStateOf(items), waivedKeysOf(items));
|
|
51
65
|
}
|
|
52
66
|
catch (error) {
|
|
53
67
|
if (error instanceof ReviewUnreachableError)
|
|
@@ -77,7 +91,16 @@ function readSubject(raw) {
|
|
|
77
91
|
type: "tool_permission",
|
|
78
92
|
toolName,
|
|
79
93
|
displayName,
|
|
80
|
-
|
|
94
|
+
...(typeof row.target === "string" || row.target === null ? { target: row.target } : {}),
|
|
95
|
+
...(typeof row.inputBytes === "number" && Number.isFinite(row.inputBytes)
|
|
96
|
+
? { inputBytes: row.inputBytes }
|
|
97
|
+
: {}),
|
|
98
|
+
...(Array.isArray(row.inputFields) && row.inputFields.every((field) => typeof field === "string")
|
|
99
|
+
? { inputFields: row.inputFields }
|
|
100
|
+
: {}),
|
|
101
|
+
...(typeof row.redactionVersion === "number" && Number.isFinite(row.redactionVersion)
|
|
102
|
+
? { redactionVersion: row.redactionVersion }
|
|
103
|
+
: {}),
|
|
81
104
|
...(typeof row.description === "string" ? { description: row.description } : {}),
|
|
82
105
|
};
|
|
83
106
|
}
|
|
@@ -117,13 +140,13 @@ export async function runReviewMirror(options, globals) {
|
|
|
117
140
|
const answered = await client.answerReviewItem(id, verdict);
|
|
118
141
|
return mirrorReport(json, "answered", answered.id);
|
|
119
142
|
}
|
|
120
|
-
const
|
|
121
|
-
if (!
|
|
122
|
-
return mirrorReport(json, "skipped", null, "--
|
|
143
|
+
const projectId = options.project?.trim() ?? "";
|
|
144
|
+
if (!projectId)
|
|
145
|
+
return mirrorReport(json, "skipped", null, "--project is required to open a mirrored prompt");
|
|
123
146
|
const subject = readSubject(options.subject);
|
|
124
147
|
if (!subject)
|
|
125
148
|
return mirrorReport(json, "skipped", null, "--subject must be a tool-permission JSON object");
|
|
126
|
-
const created = await client.createReviewItem({
|
|
149
|
+
const created = await client.createReviewItem({ projectId, sessionId, subject });
|
|
127
150
|
return mirrorReport(json, "open", created.id);
|
|
128
151
|
}
|
|
129
152
|
catch (error) {
|
|
@@ -140,9 +163,9 @@ export function registerReviewCommands(reviewCommand, getGlobalOptions) {
|
|
|
140
163
|
.action(async (options) => runReviewCheck(options, getGlobalOptions()));
|
|
141
164
|
reviewCommand
|
|
142
165
|
.command("mirror")
|
|
143
|
-
.description("Reflect a
|
|
144
|
-
.option("--session <
|
|
145
|
-
.option("--
|
|
166
|
+
.description("Reflect a session's permission prompt, or its answer, into the Review pane")
|
|
167
|
+
.option("--session <sessionId>", "the session's id")
|
|
168
|
+
.option("--project <id>", "the Project the session's folder is bound to (required with --state open)")
|
|
146
169
|
.option("--state <state>", "open or answered")
|
|
147
170
|
.option("--subject <json>", "the tool-permission subject, as JSON (required with --state open)")
|
|
148
171
|
.option("--verdict <verdict>", "allow or deny (required with --state answered)")
|
package/dist/commands/search.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolveNuStackUrl,
|
|
1
|
+
import { resolveNuStackUrl, searchProjects, searchCatalogue, } from "../lib/nustack_client.js";
|
|
2
2
|
export async function runSearch(keywords, options, globals) {
|
|
3
3
|
const query = keywords.join(" ").trim();
|
|
4
4
|
if (!query) {
|
|
@@ -12,20 +12,20 @@ export async function runSearch(keywords, options, globals) {
|
|
|
12
12
|
return;
|
|
13
13
|
}
|
|
14
14
|
const baseUrl = resolveNuStackUrl(globals);
|
|
15
|
-
const results = await
|
|
15
|
+
const results = await searchProjects(baseUrl, query, { category: options.category, limit: options.limit });
|
|
16
16
|
if (options.json) {
|
|
17
17
|
console.log(JSON.stringify(results, null, 2));
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
20
20
|
if (results.length === 0) {
|
|
21
|
-
console.log(`No
|
|
21
|
+
console.log(`No projects matched "${query}". Looking for a platform capability? Try \`nustack search services ${query}\`.`);
|
|
22
22
|
return;
|
|
23
23
|
}
|
|
24
24
|
console.log(`${results.length} result${results.length === 1 ? "" : "s"} for "${query}"`);
|
|
25
|
-
for (const
|
|
26
|
-
const category =
|
|
27
|
-
console.log(`${
|
|
28
|
-
const link =
|
|
25
|
+
for (const project of results) {
|
|
26
|
+
const category = project.categories[0] ?? "—";
|
|
27
|
+
console.log(`${project.displayName.padEnd(28)} ${category.padEnd(16)} ${project.tagline ?? ""}`);
|
|
28
|
+
const link = project.links[0];
|
|
29
29
|
if (link)
|
|
30
30
|
console.log(` ${link.url}`);
|
|
31
31
|
}
|
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import * as p from "@clack/prompts";
|
|
2
4
|
import { ensureAccessToken } from "../lib/auth_grant.js";
|
|
5
|
+
import { isInteractive } from "../lib/interactive.js";
|
|
3
6
|
import { resolveNuStackUrl } from "../lib/nustack_client.js";
|
|
4
|
-
import { makeSessionsClient } from "../lib/sessions/client.js";
|
|
7
|
+
import { makeSessionsClient, sessionsClientFromEnv } from "../lib/sessions/client.js";
|
|
5
8
|
import { recordOutsideSessions, } from "../lib/sessions/outsideSessions.js";
|
|
6
|
-
import {
|
|
9
|
+
import { inspectSession, readOrphanedWorktrees, reclaimSession, } from "../lib/session-engine/prune.js";
|
|
10
|
+
import { readSessionRegistry, sessionRegistryPath } from "../lib/session-engine/registry.js";
|
|
11
|
+
import { SessionSupervisor } from "../lib/session-engine/supervisor.js";
|
|
12
|
+
import { printJsonError, printJsonSuccess } from "../lib/json_output.js";
|
|
13
|
+
export async function runSessionsServe(engineVersion) {
|
|
14
|
+
const supervisor = new SessionSupervisor({
|
|
15
|
+
input: process.stdin,
|
|
16
|
+
output: process.stdout,
|
|
17
|
+
engineVersion,
|
|
18
|
+
sessions: sessionsClientFromEnv(process.env),
|
|
19
|
+
});
|
|
20
|
+
await supervisor.run();
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
7
23
|
async function readScanArgument(raw) {
|
|
8
24
|
const body = raw.startsWith("@") ? await readFile(raw.slice(1), "utf8") : raw;
|
|
9
25
|
return JSON.parse(body);
|
|
@@ -25,7 +41,7 @@ function readTranscripts(value) {
|
|
|
25
41
|
sessionKey,
|
|
26
42
|
provider,
|
|
27
43
|
modifiedMs,
|
|
28
|
-
|
|
44
|
+
projectId: typeof row.projectId === "string" && row.projectId ? row.projectId : null,
|
|
29
45
|
vendorSessionId: typeof row.id === "string" && row.id ? row.id : null,
|
|
30
46
|
...(typeof row.sizeBytes === "number" ? { sizeBytes: row.sizeBytes } : {}),
|
|
31
47
|
});
|
|
@@ -46,6 +62,10 @@ export async function runSessionsRecord(options, globals) {
|
|
|
46
62
|
if (!options.scan)
|
|
47
63
|
return report("sessions record", EMPTY, json, "no scan payload");
|
|
48
64
|
const payload = (await readScanArgument(options.scan).catch(() => null));
|
|
65
|
+
if (payload && "laneSessionIds" in payload) {
|
|
66
|
+
process.stderr.write("sessions record: this NuStack desktop is older than this CLI: it sent laneSessionIds, which is now engineSessionIds. Update both.\n");
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
49
69
|
const transcripts = readTranscripts(payload?.sessions);
|
|
50
70
|
if (transcripts.length === 0)
|
|
51
71
|
return report("sessions record", EMPTY, json, "nothing to record");
|
|
@@ -54,9 +74,140 @@ export async function runSessionsRecord(options, globals) {
|
|
|
54
74
|
return report("sessions record", EMPTY, json, "signed out");
|
|
55
75
|
const summary = await recordOutsideSessions(makeSessionsClient(baseUrl, deviceAuth.accessToken), {
|
|
56
76
|
transcripts,
|
|
57
|
-
|
|
58
|
-
? payload.
|
|
77
|
+
engineSessionIds: Array.isArray(payload?.engineSessionIds)
|
|
78
|
+
? payload.engineSessionIds.filter((id) => typeof id === "string")
|
|
59
79
|
: [],
|
|
60
80
|
});
|
|
61
81
|
report("sessions record", summary, json);
|
|
62
82
|
}
|
|
83
|
+
async function readSessions() {
|
|
84
|
+
const { records, dropped } = await readSessionRegistry();
|
|
85
|
+
const orphans = await readOrphanedWorktrees();
|
|
86
|
+
const sessions = [];
|
|
87
|
+
for (const record of [...records, ...orphans.records]) {
|
|
88
|
+
sessions.push({ record, facts: await inspectSession(record) });
|
|
89
|
+
}
|
|
90
|
+
return { sessions, unreadable: dropped, undecipherableOrphans: orphans.undecipherable };
|
|
91
|
+
}
|
|
92
|
+
function statusWord(entry) {
|
|
93
|
+
if (entry.facts.orphaned)
|
|
94
|
+
return "orphaned";
|
|
95
|
+
if (entry.facts.stale)
|
|
96
|
+
return "stale";
|
|
97
|
+
return entry.record.status;
|
|
98
|
+
}
|
|
99
|
+
function formatSessionRow(entry) {
|
|
100
|
+
const checkout = path.basename(entry.record.projectPath);
|
|
101
|
+
return (`${entry.record.sessionId.padEnd(38)} ${checkout.padEnd(22)} ${(entry.record.branch ?? "—").padEnd(31)} ` +
|
|
102
|
+
`${statusWord(entry).padEnd(10)} ${entry.facts.reason}`);
|
|
103
|
+
}
|
|
104
|
+
function toJsonView(entry) {
|
|
105
|
+
const { record, facts } = entry;
|
|
106
|
+
return {
|
|
107
|
+
sessionId: record.sessionId,
|
|
108
|
+
projectPath: record.projectPath,
|
|
109
|
+
worktreePath: record.worktreePath,
|
|
110
|
+
branch: record.branch,
|
|
111
|
+
status: record.status,
|
|
112
|
+
nustack: record.nustack,
|
|
113
|
+
createdAt: record.createdAt,
|
|
114
|
+
closedAt: record.closedAt ?? null,
|
|
115
|
+
permissionMode: record.permissionMode ?? null,
|
|
116
|
+
baseBranch: record.baseBranch ?? null,
|
|
117
|
+
isolated: facts.isolated,
|
|
118
|
+
checkoutPresent: facts.checkoutPresent,
|
|
119
|
+
worktreePresent: facts.worktreePresent,
|
|
120
|
+
branchExists: facts.branchExists,
|
|
121
|
+
reading: facts.reading,
|
|
122
|
+
dirty: facts.dirty,
|
|
123
|
+
stale: facts.stale,
|
|
124
|
+
orphaned: facts.orphaned,
|
|
125
|
+
reclaimable: facts.reclaimable,
|
|
126
|
+
reason: facts.reason,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
export async function runSessionsList(options) {
|
|
130
|
+
const json = options.json === true;
|
|
131
|
+
const { sessions, unreadable, undecipherableOrphans } = await readSessions();
|
|
132
|
+
if (json) {
|
|
133
|
+
printJsonSuccess("sessions list", {
|
|
134
|
+
registryPath: sessionRegistryPath(),
|
|
135
|
+
unreadable,
|
|
136
|
+
undecipherableOrphans,
|
|
137
|
+
sessions: sessions.map(toJsonView),
|
|
138
|
+
});
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (sessions.length === 0 && unreadable === 0 && undecipherableOrphans.length === 0) {
|
|
142
|
+
console.log("No engine-opened sessions on this machine.");
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (sessions.length > 0) {
|
|
146
|
+
console.log(`${"SESSION".padEnd(38)} ${"CHECKOUT".padEnd(22)} ${"BRANCH".padEnd(31)} ${"STATUS".padEnd(10)} HOLDS`);
|
|
147
|
+
for (const entry of sessions)
|
|
148
|
+
console.log(formatSessionRow(entry));
|
|
149
|
+
}
|
|
150
|
+
if (unreadable > 0)
|
|
151
|
+
console.log(`${unreadable} row(s) in the registry could not be read and were skipped.`);
|
|
152
|
+
for (const orphan of undecipherableOrphans) {
|
|
153
|
+
console.log(`1 worktree under the pre-rename engine home names no checkout and must be removed by hand: ${orphan}.`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function previewLine(entry) {
|
|
157
|
+
const verb = entry.facts.reclaimable ? "reclaim" : "refuse";
|
|
158
|
+
return ` ${verb} ${entry.record.sessionId} ${entry.record.branch ?? "—"} — ${entry.facts.reason}`;
|
|
159
|
+
}
|
|
160
|
+
export async function runSessionsPrune(options) {
|
|
161
|
+
const json = options.json === true;
|
|
162
|
+
const force = options.force === true;
|
|
163
|
+
const { sessions } = await readSessions();
|
|
164
|
+
const scoped = options.session ? sessions.filter((entry) => entry.record.sessionId === options.session) : sessions;
|
|
165
|
+
if (options.session && scoped.length === 0) {
|
|
166
|
+
const message = `No session ${options.session} in this machine's registry — pass one from \`nustack sessions list\`.`;
|
|
167
|
+
if (json)
|
|
168
|
+
return printJsonError("sessions prune", { code: "SESSION_NOT_FOUND", message });
|
|
169
|
+
p.log.error(message);
|
|
170
|
+
process.exitCode = 1;
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const candidates = scoped.filter((entry) => force || entry.facts.reclaimable);
|
|
174
|
+
if (options.yes !== true) {
|
|
175
|
+
const plan = candidates.map((entry) => ({
|
|
176
|
+
sessionId: entry.record.sessionId,
|
|
177
|
+
branch: entry.record.branch,
|
|
178
|
+
worktreePath: entry.record.worktreePath,
|
|
179
|
+
orphaned: entry.facts.orphaned,
|
|
180
|
+
reclaimable: entry.facts.reclaimable,
|
|
181
|
+
reason: entry.facts.reason,
|
|
182
|
+
}));
|
|
183
|
+
if (json)
|
|
184
|
+
return printJsonSuccess("sessions prune", { applied: false, plan, note: "preview only — pass --yes to apply" });
|
|
185
|
+
if (plan.length === 0) {
|
|
186
|
+
console.log("Nothing to reclaim.");
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
console.log(`Would reclaim ${plan.length} session(s):`);
|
|
190
|
+
for (const entry of candidates)
|
|
191
|
+
console.log(previewLine(entry));
|
|
192
|
+
if (!isInteractive()) {
|
|
193
|
+
console.log("Preview only — re-run with --yes to apply.");
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const answer = await p.confirm({ message: "Remove these worktrees and delete their branches?" });
|
|
197
|
+
if (p.isCancel(answer) || !answer) {
|
|
198
|
+
p.outro("Cancelled — nothing reclaimed.");
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const reclaimed = [];
|
|
203
|
+
const refused = [];
|
|
204
|
+
for (const entry of candidates) {
|
|
205
|
+
const outcome = await reclaimSession(entry.record, { force });
|
|
206
|
+
(outcome.reason === null ? reclaimed : refused).push(outcome);
|
|
207
|
+
}
|
|
208
|
+
if (json)
|
|
209
|
+
return printJsonSuccess("sessions prune", { applied: true, reclaimed, refused });
|
|
210
|
+
console.log(`Reclaimed ${reclaimed.length} session(s); refused ${refused.length}.`);
|
|
211
|
+
for (const outcome of refused)
|
|
212
|
+
console.log(` refused ${outcome.sessionId} — ${outcome.reason ?? "unknown"}`);
|
|
213
|
+
}
|
package/dist/commands/sync.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import * as p from "@clack/prompts";
|
|
3
|
-
import { exchangeServiceCredential, fetchServiceCredential,
|
|
3
|
+
import { exchangeServiceCredential, fetchServiceCredential, listProjectIntegrations, resolveNuStackUrl, } from "../lib/nustack_client.js";
|
|
4
4
|
import { readConsumesEdges } from "../lib/manifest.js";
|
|
5
5
|
import { ensureAccessToken, signedOutCopy } from "../lib/auth_grant.js";
|
|
6
|
-
import {
|
|
6
|
+
import { resolveProjectId } from "../lib/project_context.js";
|
|
7
7
|
import { readManagedValues, writeManagedValues } from "../lib/env_file.js";
|
|
8
8
|
import { printJsonError, printJsonSuccess } from "../lib/json_output.js";
|
|
9
9
|
const COMMAND = "sync";
|
|
@@ -18,23 +18,23 @@ export async function runSync(options) {
|
|
|
18
18
|
if (!deviceAuth)
|
|
19
19
|
return failSync(json, "SIGNED_OUT", signedOut(), "The managed block was not touched.");
|
|
20
20
|
const accessToken = deviceAuth.accessToken;
|
|
21
|
-
const
|
|
22
|
-
if (!
|
|
23
|
-
return failSync(json, "
|
|
21
|
+
const project = await resolveProjectId(cwd, { project: options.project });
|
|
22
|
+
if (!project.ok)
|
|
23
|
+
return failSync(json, "PROJECT_UNRESOLVED", project.message, "The managed block was not touched.");
|
|
24
24
|
const spinner = json ? null : p.spinner();
|
|
25
25
|
spinner?.start("Exchanging declared services");
|
|
26
26
|
let activeServices;
|
|
27
27
|
let projections;
|
|
28
28
|
try {
|
|
29
29
|
for (const edge of await readConsumesEdges(cwd)) {
|
|
30
|
-
await exchangeServiceCredential(baseUrl, accessToken, {
|
|
30
|
+
await exchangeServiceCredential(baseUrl, accessToken, { projectId: project.projectId, service: edge.service });
|
|
31
31
|
}
|
|
32
32
|
spinner?.message("Enumerating active integrations");
|
|
33
|
-
const integrations = await
|
|
33
|
+
const integrations = await listProjectIntegrations(baseUrl, accessToken, project.projectId);
|
|
34
34
|
activeServices = integrations.filter((i) => i.status === "active").map((i) => i.serviceSlug);
|
|
35
35
|
projections = [];
|
|
36
36
|
for (const service of activeServices) {
|
|
37
|
-
projections.push(await fetchServiceCredential(baseUrl, accessToken, {
|
|
37
|
+
projections.push(await fetchServiceCredential(baseUrl, accessToken, { projectId: project.projectId, service }));
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
catch (error) {
|
|
@@ -59,7 +59,7 @@ export async function runSync(options) {
|
|
|
59
59
|
await writeManagedValues(path.join(cwd, ".env.example"), placeholders);
|
|
60
60
|
if (json) {
|
|
61
61
|
printJsonSuccess(COMMAND, {
|
|
62
|
-
|
|
62
|
+
projectId: project.projectId,
|
|
63
63
|
integrations: activeServices.map((service) => ({ service, status: "active" })),
|
|
64
64
|
envKeysWritten: Object.keys(nextValues),
|
|
65
65
|
removedKeys,
|
|
@@ -69,7 +69,7 @@ export async function runSync(options) {
|
|
|
69
69
|
p.log.success(`Managed block rewritten: ${projections.length} service credential(s).`);
|
|
70
70
|
if (removedKeys.length > 0)
|
|
71
71
|
p.log.info(`Removed stale entries: ${removedKeys.join(", ")}`);
|
|
72
|
-
p.outro("In sync with NuStack — Service credentials only (Documents & Git are `nustack
|
|
72
|
+
p.outro("In sync with NuStack — Service credentials only (Documents & Git are `nustack project sync`).");
|
|
73
73
|
}
|
|
74
74
|
function failSync(json, code, message, humanTail) {
|
|
75
75
|
if (json) {
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { createPersonalToken, listPersonalTokens, PersonalTokenRequestError, resolveNuStackUrl, revokePersonalToken } from "../lib/nustack_client.js";
|
|
2
|
+
import { ensureAuthWithRecovery, lastAuthFailure } from "../lib/auth_recovery.js";
|
|
3
|
+
import { printJsonError, printJsonSuccess } from "../lib/json_output.js";
|
|
4
|
+
const MAX_EXPIRY_DAYS = 365;
|
|
5
|
+
async function requireAuth(command, json, globals) {
|
|
6
|
+
const baseUrl = resolveNuStackUrl(globals);
|
|
7
|
+
const auth = await ensureAuthWithRecovery(baseUrl, json);
|
|
8
|
+
if (!auth) {
|
|
9
|
+
const failure = lastAuthFailure();
|
|
10
|
+
if (json)
|
|
11
|
+
printJsonError(command, { code: failure.code, message: failure.message });
|
|
12
|
+
else {
|
|
13
|
+
console.error(failure.message);
|
|
14
|
+
process.exitCode = 1;
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
return { baseUrl, accessToken: auth.accessToken };
|
|
19
|
+
}
|
|
20
|
+
function refuse(command, json, error) {
|
|
21
|
+
const code = error instanceof PersonalTokenRequestError ? error.code : "REQUEST_FAILED";
|
|
22
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
23
|
+
if (json)
|
|
24
|
+
printJsonError(command, { code, message });
|
|
25
|
+
else {
|
|
26
|
+
console.error(message);
|
|
27
|
+
process.exitCode = 1;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export async function runTokenCreate(options, globals) {
|
|
31
|
+
const json = options.json === true;
|
|
32
|
+
const label = options.label?.trim() ?? "";
|
|
33
|
+
if (!label)
|
|
34
|
+
return refuse("token create", json, new PersonalTokenRequestError("VALIDATION_FAILED", "A label is required: `nustack token create --label <name>`."));
|
|
35
|
+
let expiresInDays;
|
|
36
|
+
if (options.expires !== undefined) {
|
|
37
|
+
const parsed = Number(options.expires);
|
|
38
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > MAX_EXPIRY_DAYS) {
|
|
39
|
+
return refuse("token create", json, new PersonalTokenRequestError("VALIDATION_FAILED", `--expires is a whole number of days, 1–${MAX_EXPIRY_DAYS}.`));
|
|
40
|
+
}
|
|
41
|
+
expiresInDays = parsed;
|
|
42
|
+
}
|
|
43
|
+
const auth = await requireAuth("token create", json, globals);
|
|
44
|
+
if (!auth)
|
|
45
|
+
return;
|
|
46
|
+
try {
|
|
47
|
+
const minted = await createPersonalToken(auth.baseUrl, auth.accessToken, { label, ...(expiresInDays ? { expiresInDays } : {}) });
|
|
48
|
+
if (json) {
|
|
49
|
+
printJsonSuccess("token create", minted);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
console.error(`Minted \`${minted.token.label}\` (${minted.token.tokenPrefix}…)${minted.token.expiresAt ? `, expires ${minted.token.expiresAt}` : ""}.`);
|
|
53
|
+
console.error(`This is the only time the secret is shown. Store it as ${"NUSTACK_TOKEN"} in your pipeline; revoke it with \`nustack token revoke ${minted.token.id}\`.`);
|
|
54
|
+
console.log(minted.secret);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
refuse("token create", json, error);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export async function runTokenList(options, globals) {
|
|
61
|
+
const json = options.json === true;
|
|
62
|
+
const auth = await requireAuth("token list", json, globals);
|
|
63
|
+
if (!auth)
|
|
64
|
+
return;
|
|
65
|
+
try {
|
|
66
|
+
const tokens = await listPersonalTokens(auth.baseUrl, auth.accessToken);
|
|
67
|
+
if (json) {
|
|
68
|
+
printJsonSuccess("token list", { tokens });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (tokens.length === 0) {
|
|
72
|
+
console.log("No personal access tokens. Mint one with `nustack token create --label <name>`.");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
for (const token of tokens) {
|
|
76
|
+
const state = token.revokedAt ? `revoked ${token.revokedAt}` : token.expiresAt ? `expires ${token.expiresAt}` : "no expiry";
|
|
77
|
+
console.log(`${token.id.padEnd(38)} ${`${token.tokenPrefix}…`.padEnd(18)} ${token.label.padEnd(24)} ${state.padEnd(30)} last used ${token.lastUsedAt ?? "never"}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
refuse("token list", json, error);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export async function runTokenRevoke(tokenId, options, globals) {
|
|
85
|
+
const json = options.json === true;
|
|
86
|
+
if (!tokenId?.trim())
|
|
87
|
+
return refuse("token revoke", json, new PersonalTokenRequestError("VALIDATION_FAILED", "A token id is required: `nustack token revoke <id>` (see `nustack token list`)."));
|
|
88
|
+
const auth = await requireAuth("token revoke", json, globals);
|
|
89
|
+
if (!auth)
|
|
90
|
+
return;
|
|
91
|
+
try {
|
|
92
|
+
const revoked = await revokePersonalToken(auth.baseUrl, auth.accessToken, tokenId.trim());
|
|
93
|
+
if (json) {
|
|
94
|
+
printJsonSuccess("token revoke", revoked);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
console.log(`Revoked ${revoked.id} at ${revoked.revokedAt}.`);
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
refuse("token revoke", json, error);
|
|
101
|
+
}
|
|
102
|
+
}
|
package/dist/commands/toolkit.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import * as p from "@clack/prompts";
|
|
2
2
|
import { resolveNuStackUrl } from "../lib/nustack_client.js";
|
|
3
3
|
import { ensureAuthWithRecovery, lastAuthFailure } from "../lib/auth_recovery.js";
|
|
4
|
-
import {
|
|
4
|
+
import { resolveProjectId } from "../lib/project_context.js";
|
|
5
5
|
import { isInteractive } from "../lib/interactive.js";
|
|
6
6
|
import { openLocalStore } from "../lib/local_store/database.js";
|
|
7
7
|
import { getBindingByRepositoryRoot } from "../lib/local_store/bindings.js";
|
|
8
8
|
import { resolveRepository } from "../lib/git-sync/core/repository.js";
|
|
9
|
-
import {
|
|
9
|
+
import { readProjectManifests, diagnoseManifests } from "../lib/onboarding/manifest_writer.js";
|
|
10
10
|
import { fetchToolkitProfile, ToolkitProfileUnavailableError } from "../lib/etna/profile.js";
|
|
11
11
|
import { buildReconcilePlan, applyReconcile, toolkitCompatibility } from "../lib/etna/reconcile.js";
|
|
12
12
|
import { printJsonError, printJsonSuccess } from "../lib/json_output.js";
|
|
@@ -28,9 +28,9 @@ async function resolveContext(command, json, options, globals) {
|
|
|
28
28
|
return null;
|
|
29
29
|
}
|
|
30
30
|
const baseUrl = resolveNuStackUrl(globals);
|
|
31
|
-
const
|
|
32
|
-
if (!
|
|
33
|
-
emitFail(command, json, "
|
|
31
|
+
const project = await resolveProjectId(root, options);
|
|
32
|
+
if (!project.ok) {
|
|
33
|
+
emitFail(command, json, "PROJECT_CONTEXT_MISSING", project.message);
|
|
34
34
|
return null;
|
|
35
35
|
}
|
|
36
36
|
const deviceAuth = await ensureAuthWithRecovery(baseUrl, json);
|
|
@@ -39,7 +39,7 @@ async function resolveContext(command, json, options, globals) {
|
|
|
39
39
|
emitFail(command, json, failure.code, failure.message);
|
|
40
40
|
return null;
|
|
41
41
|
}
|
|
42
|
-
return { baseUrl, accessToken: deviceAuth.accessToken,
|
|
42
|
+
return { baseUrl, accessToken: deviceAuth.accessToken, projectId: project.projectId, root };
|
|
43
43
|
}
|
|
44
44
|
export async function runToolkitStatus(options, globals) {
|
|
45
45
|
const json = options.json === true;
|
|
@@ -48,7 +48,7 @@ export async function runToolkitStatus(options, globals) {
|
|
|
48
48
|
return;
|
|
49
49
|
let cloud;
|
|
50
50
|
try {
|
|
51
|
-
cloud = await fetchToolkitProfile(ctx.baseUrl, ctx.accessToken, ctx.
|
|
51
|
+
cloud = await fetchToolkitProfile(ctx.baseUrl, ctx.accessToken, ctx.projectId);
|
|
52
52
|
}
|
|
53
53
|
catch (error) {
|
|
54
54
|
if (error instanceof ToolkitProfileUnavailableError) {
|
|
@@ -57,24 +57,24 @@ export async function runToolkitStatus(options, globals) {
|
|
|
57
57
|
}
|
|
58
58
|
throw error;
|
|
59
59
|
}
|
|
60
|
-
const manifests = await
|
|
61
|
-
const diagnosis = diagnoseManifests({ name: cloud.name, version: cloud.version }, manifests.
|
|
60
|
+
const manifests = await readProjectManifests(ctx.root);
|
|
61
|
+
const diagnosis = diagnoseManifests({ name: cloud.name, version: cloud.version }, manifests.projectManifest?.toolkit ?? null, manifests.serviceToolkit);
|
|
62
62
|
const binding = await withStore((store) => getBindingByRepositoryRoot(store, ctx.root));
|
|
63
63
|
const compatibility = toolkitCompatibility(binding ?? { etnaProfileJson: null }, cloud);
|
|
64
64
|
if (json) {
|
|
65
65
|
printJsonSuccess("toolkit status", {
|
|
66
|
-
|
|
66
|
+
projectId: ctx.projectId,
|
|
67
67
|
cloud,
|
|
68
|
-
|
|
68
|
+
projectJson: manifests.projectManifest?.toolkit ?? null,
|
|
69
69
|
serviceJson: manifests.serviceToolkit,
|
|
70
70
|
diagnosis,
|
|
71
71
|
compatibility,
|
|
72
72
|
});
|
|
73
73
|
return;
|
|
74
74
|
}
|
|
75
|
-
console.log(`Toolkit for
|
|
75
|
+
console.log(`Toolkit for Project ${ctx.projectId}`);
|
|
76
76
|
console.log(` cloud pin: ${cloud.name}@${cloud.version} (renderer ${cloud.rendererVersion}, placement ${cloud.placementMapVersion})`);
|
|
77
|
-
console.log(` .nustack/app.json: ${fmtToolkit(manifests.
|
|
77
|
+
console.log(` .nustack/app.json: ${fmtToolkit(manifests.projectManifest?.toolkit)}`);
|
|
78
78
|
console.log(` nustack.service.json:${fmtToolkit(manifests.serviceToolkit)}`);
|
|
79
79
|
console.log(` diagnosis: ${diagnosis}`);
|
|
80
80
|
console.log(` write gate: ${compatibility.blocksWrites ? `BLOCKED — ${compatibility.reason}` : "clear (projection and sync writes allowed)"}`);
|
|
@@ -89,7 +89,7 @@ export async function runToolkitReconcile(options, globals) {
|
|
|
89
89
|
return;
|
|
90
90
|
let cloud;
|
|
91
91
|
try {
|
|
92
|
-
cloud = await fetchToolkitProfile(ctx.baseUrl, ctx.accessToken, ctx.
|
|
92
|
+
cloud = await fetchToolkitProfile(ctx.baseUrl, ctx.accessToken, ctx.projectId);
|
|
93
93
|
}
|
|
94
94
|
catch (error) {
|
|
95
95
|
if (error instanceof ToolkitProfileUnavailableError) {
|
|
@@ -99,12 +99,12 @@ export async function runToolkitReconcile(options, globals) {
|
|
|
99
99
|
throw error;
|
|
100
100
|
}
|
|
101
101
|
const binding = await withStore((store) => getBindingByRepositoryRoot(store, ctx.root));
|
|
102
|
-
const manifests = await
|
|
103
|
-
const identity = manifests.
|
|
104
|
-
? {
|
|
105
|
-
: manifests.
|
|
102
|
+
const manifests = await readProjectManifests(ctx.root);
|
|
103
|
+
const identity = manifests.projectManifest
|
|
104
|
+
? { projectId: manifests.projectManifest.workspaceId, organizationId: manifests.projectManifest.organizationId }
|
|
105
|
+
: manifests.serviceProjectBinding;
|
|
106
106
|
if (!identity) {
|
|
107
|
-
emitFail("toolkit reconcile", json, "
|
|
107
|
+
emitFail("toolkit reconcile", json, "PROJECT_CONTEXT_MISSING", "No `.nustack/app.json` binding to reconcile — attach the repository first.");
|
|
108
108
|
return;
|
|
109
109
|
}
|
|
110
110
|
const plan = await buildReconcilePlan(ctx.root, binding ?? { etnaProfileJson: null }, cloud);
|