@nurix/nustack 0.23.12 → 0.23.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/agent.js +1 -53
- package/dist/commands/check.js +73 -0
- package/dist/commands/deploy.js +2 -1
- package/dist/commands/doctor.js +2 -1
- package/dist/commands/harness.js +127 -48
- package/dist/commands/init.js +103 -3
- package/dist/commands/login.js +9 -2
- package/dist/commands/mcp.js +7 -0
- package/dist/commands/project-sync.js +2 -1
- package/dist/commands/read.js +19 -0
- package/dist/commands/records.js +10 -0
- package/dist/commands/review.js +3 -2
- package/dist/commands/sessions.js +4 -1
- package/dist/commands/sync.js +2 -1
- package/dist/index.js +4 -0
- package/dist/lib/agent/claudeHooks.js +4 -1
- package/dist/lib/agent/desktopDoor.js +55 -0
- package/dist/lib/auth_grant.js +15 -3
- package/dist/lib/auth_recovery.js +10 -3
- package/dist/lib/etna/manager.js +24 -6
- package/dist/lib/etna/profile.js +6 -0
- package/dist/lib/etna/reconcile.js +6 -0
- package/dist/lib/gate/bulkReadRelay.js +354 -0
- package/dist/lib/gate/hook.js +30 -3
- package/dist/lib/gate/outline.js +12 -0
- package/dist/lib/gate/settings.js +24 -1
- package/dist/lib/gate/tierOne.js +22 -0
- package/dist/lib/harness/codes.js +24 -0
- package/dist/lib/harness/hookCheck.js +325 -0
- package/dist/lib/harness/hookEntry.js +27 -0
- package/dist/lib/harness/legacyMount.js +231 -0
- package/dist/lib/harness/mount.js +199 -131
- package/dist/lib/harness/storeWaivers.js +43 -0
- package/dist/lib/harness/vendor.js +29 -0
- package/dist/lib/harness/versionsClient.js +54 -0
- package/dist/lib/local_store/database.js +18 -0
- package/dist/lib/local_store/routing.js +26 -0
- package/dist/lib/nustack_client.js +35 -2
- package/dist/lib/onboarding/attach_existing_git.js +14 -2
- package/dist/lib/onboarding/binding.js +46 -2
- package/dist/lib/onboarding/commit_trailer_hook.js +66 -0
- package/dist/lib/onboarding/manifest_writer.js +19 -3
- package/dist/lib/onboarding/session_hook.js +76 -0
- package/dist/lib/origin_guard.js +43 -0
- package/dist/lib/project_contract.js +104 -0
- package/dist/lib/records/door.js +26 -19
- package/dist/lib/records/drain.js +10 -4
- package/dist/lib/records/refusalLog.js +30 -1
- package/dist/lib/session-engine/supervisor.js +6 -18
- package/dist/lib/sessions/client.js +0 -12
- package/dist/program.js +27 -4
- package/dist/vendor/harness/assemble/runtimes.js +32 -0
- package/dist/vendor/harness/assemble/settingsMerge.js +136 -0
- package/dist/vendor/harness/digest/index.js +38 -0
- package/dist/vendor/harness/lib/findings.js +32 -0
- package/dist/vendor/harness/lib/frontmatter.js +79 -0
- package/dist/vendor/harness/lib/paths.js +31 -0
- package/dist/vendor/harness/mount/cache.js +65 -0
- package/dist/vendor/harness/mount/exclude.js +77 -0
- package/dist/vendor/harness/mount/files.js +38 -0
- package/dist/vendor/harness/mount/index.js +352 -0
- package/dist/vendor/harness/rules/changeRules.js +162 -0
- package/dist/vendor/harness/rules/context.js +38 -0
- package/dist/vendor/harness/rules/docsRoot.js +34 -0
- package/dist/vendor/harness/rules/index.js +109 -0
- package/dist/vendor/harness/rules/node.js +144 -0
- package/dist/vendor/harness/rules/outputGate.js +28 -0
- package/dist/vendor/harness/rules/ownership.js +178 -0
- package/dist/vendor/harness/rules/packageTable.js +113 -0
- package/dist/vendor/harness/rules/plans.js +96 -0
- package/dist/vendor/harness/rules/sessionWalls.js +31 -0
- package/package.json +5 -5
package/dist/commands/agent.js
CHANGED
|
@@ -1,59 +1,7 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
1
|
import os from "node:os";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
import { installSessionStartHook, removeSessionStartHook } from "../lib/agent/claudeHooks.js";
|
|
5
|
-
|
|
6
|
-
function desktopConfigDir() {
|
|
7
|
-
return (process.env.NUSTACK_DESKTOP_CONFIG_DIR ||
|
|
8
|
-
path.join(os.homedir(), "Library", "Application Support", "co.nurix.nustack"));
|
|
9
|
-
}
|
|
10
|
-
async function readServerRecord() {
|
|
11
|
-
const recordPath = path.join(desktopConfigDir(), "server.json");
|
|
12
|
-
try {
|
|
13
|
-
const parsed = JSON.parse(await readFile(recordPath, "utf8"));
|
|
14
|
-
if (typeof parsed.port !== "number" ||
|
|
15
|
-
typeof parsed.token !== "string" ||
|
|
16
|
-
typeof parsed.pid !== "number" ||
|
|
17
|
-
typeof parsed.startedAt !== "string") {
|
|
18
|
-
return null;
|
|
19
|
-
}
|
|
20
|
-
return parsed;
|
|
21
|
-
}
|
|
22
|
-
catch {
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
function isPidAlive(pid) {
|
|
27
|
-
try {
|
|
28
|
-
process.kill(pid, 0);
|
|
29
|
-
return true;
|
|
30
|
-
}
|
|
31
|
-
catch (error) {
|
|
32
|
-
return error.code === "EPERM";
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
function isRosterProject(value) {
|
|
36
|
-
return (value !== null &&
|
|
37
|
-
typeof value === "object" &&
|
|
38
|
-
typeof value.path === "string" &&
|
|
39
|
-
typeof value.label === "string");
|
|
40
|
-
}
|
|
41
|
-
async function probeProjects(record) {
|
|
42
|
-
try {
|
|
43
|
-
const response = await fetch(`http://127.0.0.1:${record.port}/api/projects`, {
|
|
44
|
-
headers: { Authorization: `Bearer ${record.token}` },
|
|
45
|
-
signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),
|
|
46
|
-
});
|
|
47
|
-
if (!response.ok)
|
|
48
|
-
return null;
|
|
49
|
-
const body = (await response.json());
|
|
50
|
-
const projects = body.data?.projects;
|
|
51
|
-
return Array.isArray(projects) ? projects.filter(isRosterProject) : null;
|
|
52
|
-
}
|
|
53
|
-
catch {
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
4
|
+
import { isPidAlive, probeProjects, readServerRecord } from "../lib/agent/desktopDoor.js";
|
|
57
5
|
function describeContext(record, projects) {
|
|
58
6
|
const baseUrl = `http://127.0.0.1:${record.port}`;
|
|
59
7
|
const roster = projects.length
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { resolveRepository } from "../lib/git-sync/core/repository.js";
|
|
3
|
+
import { loadHarness } from "../lib/harness/vendor.js";
|
|
4
|
+
import { readStoreDocumentationWaivers } from "../lib/harness/storeWaivers.js";
|
|
5
|
+
import { runHookMode } from "../lib/harness/hookEntry.js";
|
|
6
|
+
import { readProjectContract } from "../lib/project_contract.js";
|
|
7
|
+
import { NUSTACK_JSON_VERSION } from "../lib/json_output.js";
|
|
8
|
+
export const CHECK_EXIT = { PASS: 0, FAILED: 1, CANNOT_RUN: 2 };
|
|
9
|
+
const COMMAND = "check";
|
|
10
|
+
function cannotRun(json, rule, code, message) {
|
|
11
|
+
if (json)
|
|
12
|
+
console.log(JSON.stringify({ nustackJsonVersion: NUSTACK_JSON_VERSION, command: COMMAND, ok: false, error: { code, message }, data: { rule, verdict: null, findings: [] } }));
|
|
13
|
+
else
|
|
14
|
+
console.error(message);
|
|
15
|
+
process.exitCode = CHECK_EXIT.CANNOT_RUN;
|
|
16
|
+
}
|
|
17
|
+
function rootOf(dir) {
|
|
18
|
+
try {
|
|
19
|
+
return resolveRepository(dir).repositoryRoot;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return dir;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export async function runCheck(rule, options) {
|
|
26
|
+
if (options.hook !== undefined) {
|
|
27
|
+
await runHookMode(options.hook, options.protection === true);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const json = options.json === true;
|
|
31
|
+
if (!rule) {
|
|
32
|
+
cannotRun(json, null, "VALIDATION_FAILED", "Name a rule: `nustack check <rule>`, or pass `--hook <event>`.");
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
let rules;
|
|
36
|
+
let rulesNode;
|
|
37
|
+
try {
|
|
38
|
+
rules = await loadHarness("rules");
|
|
39
|
+
rulesNode = await loadHarness("rulesNode");
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
cannotRun(json, rule, "HARNESS_UNAVAILABLE", `The rule library did not load: ${error instanceof Error ? error.message : String(error)}`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (!rules.isRuleId(rule)) {
|
|
46
|
+
cannotRun(json, rule, "UNKNOWN_RULE", `Unknown rule \`${rule}\`; this CLI serves ${rules.RULE_IDS.join(", ")}.`);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const root = rootOf(path.resolve(process.cwd()));
|
|
50
|
+
let result;
|
|
51
|
+
try {
|
|
52
|
+
const waivers = rulesNode.readBookkeepingWaivers(root);
|
|
53
|
+
if (rule === "bookkeeping") {
|
|
54
|
+
const contract = await readProjectContract(root);
|
|
55
|
+
waivers.push(...(await readStoreDocumentationWaivers(contract.projectId)));
|
|
56
|
+
}
|
|
57
|
+
result = rules.runRule(rule, { ...rulesNode.nodeRuleContext({ root }), waivers });
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
cannotRun(json, rule, "RULE_CANNOT_RUN", `${rule} could not run: ${error instanceof Error ? error.message : String(error)}`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const exit = result.verdict === "pass" ? CHECK_EXIT.PASS : CHECK_EXIT.FAILED;
|
|
64
|
+
if (json) {
|
|
65
|
+
console.log(JSON.stringify({ nustackJsonVersion: NUSTACK_JSON_VERSION, command: COMMAND, ok: true, data: { rule, verdict: result.verdict, findings: result.findings } }));
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
console.log(`${rule}: ${result.verdict}`);
|
|
69
|
+
for (const finding of result.findings)
|
|
70
|
+
console.log(` ${finding.path ? `${finding.path}: ` : ""}${finding.message}`);
|
|
71
|
+
}
|
|
72
|
+
process.exitCode = exit;
|
|
73
|
+
}
|
package/dist/commands/deploy.js
CHANGED
|
@@ -6,6 +6,7 @@ import { printJsonError, printJsonSuccess } from "../lib/json_output.js";
|
|
|
6
6
|
import { resolveNuStackUrl } from "../lib/nustack_client.js";
|
|
7
7
|
import { resolveProjectId } from "../lib/project_context.js";
|
|
8
8
|
import { projectOverviewJson, readProjectOverview } from "./project.js";
|
|
9
|
+
import { nullUnlessOriginRefused } from "../lib/origin_guard.js";
|
|
9
10
|
const DEFAULT_TIMEOUT_MINUTES = 15;
|
|
10
11
|
export function emitFail(command, json, code, message) {
|
|
11
12
|
if (json)
|
|
@@ -173,7 +174,7 @@ export async function runStatus(options, globals) {
|
|
|
173
174
|
return;
|
|
174
175
|
}
|
|
175
176
|
const baseUrl = resolveNuStackUrl(globals);
|
|
176
|
-
const auth = await ensureAccessToken().catch(
|
|
177
|
+
const auth = await ensureAccessToken().catch(nullUnlessOriginRefused);
|
|
177
178
|
const overview = await readProjectOverview(project.projectId, cwd, baseUrl, auth?.accessToken ?? null);
|
|
178
179
|
let hosting = null;
|
|
179
180
|
if (auth && overview.cloudProject) {
|
package/dist/commands/doctor.js
CHANGED
|
@@ -21,6 +21,7 @@ import { NotARepositoryError } from "../lib/git-sync/core/git_runner.js";
|
|
|
21
21
|
import { listBundles, sweepExpiredBundles } from "../lib/git-sync/recovery/store.js";
|
|
22
22
|
import { inspectRecoveryKey } from "../lib/git-sync/recovery/crypto.js";
|
|
23
23
|
import { NUSTACK_JSON_VERSION } from "../lib/json_output.js";
|
|
24
|
+
import { nullUnlessOriginRefused } from "../lib/origin_guard.js";
|
|
24
25
|
async function credentialStoreRows(environmentInUse) {
|
|
25
26
|
if (environmentInUse)
|
|
26
27
|
return [{ check: "Credential source", status: "ok", detail: "environment — no store is read" }];
|
|
@@ -58,7 +59,7 @@ export async function runDoctor(options) {
|
|
|
58
59
|
rows.push({ area, check, status, detail });
|
|
59
60
|
};
|
|
60
61
|
const fromEnv = envToken();
|
|
61
|
-
const deviceAuth = await ensureAccessToken().catch(
|
|
62
|
+
const deviceAuth = await ensureAccessToken().catch(nullUnlessOriginRefused);
|
|
62
63
|
if (fromEnv?.kind === "malformed")
|
|
63
64
|
add("identity", "Device sign-in", "error", fromEnv.message);
|
|
64
65
|
else if (fromEnv)
|
package/dist/commands/harness.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { getProject, resolveNuStackUrl } from "../lib/nustack_client.js";
|
|
3
|
-
import { ensureAuthWithRecovery, lastAuthFailure } from "../lib/auth_recovery.js";
|
|
4
|
-
import { resolveProjectId } from "../lib/project_context.js";
|
|
5
2
|
import { resolveRepository } from "../lib/git-sync/core/repository.js";
|
|
6
|
-
import {
|
|
3
|
+
import { isProjectVersionSummary, mountProjectHarness, projectOf, readMountState, readSummary } from "../lib/harness/mount.js";
|
|
4
|
+
import { readLegacyMountRecord } from "../lib/harness/legacyMount.js";
|
|
5
|
+
import { resolveProjectVersion } from "../lib/harness/versionsClient.js";
|
|
6
|
+
import { HARNESS_CODES, STATUS_EXIT } from "../lib/harness/codes.js";
|
|
7
|
+
import { installCommitTrailerHook, trailerHookLine } from "../lib/onboarding/commit_trailer_hook.js";
|
|
8
|
+
import { readProjectContract } from "../lib/project_contract.js";
|
|
7
9
|
import { printJsonError, printJsonSuccess } from "../lib/json_output.js";
|
|
10
|
+
const RUNTIMES = ["claude-code", "codex", "opencode", "copilot-cli"];
|
|
11
|
+
const WRITE_CONTRACT_MODES = ["never", "when-unmodified"];
|
|
8
12
|
function emitFail(command, json, code, message) {
|
|
9
13
|
if (json)
|
|
10
14
|
printJsonError(command, { code, message });
|
|
@@ -13,6 +17,10 @@ function emitFail(command, json, code, message) {
|
|
|
13
17
|
process.exitCode = 1;
|
|
14
18
|
}
|
|
15
19
|
}
|
|
20
|
+
function codeOf(error) {
|
|
21
|
+
const code = error?.code;
|
|
22
|
+
return typeof code === "string" ? code : HARNESS_CODES.HARNESS_MOUNT_FAILED;
|
|
23
|
+
}
|
|
16
24
|
function rootOf(dir) {
|
|
17
25
|
try {
|
|
18
26
|
return resolveRepository(dir).repositoryRoot;
|
|
@@ -21,70 +29,141 @@ function rootOf(dir) {
|
|
|
21
29
|
return dir;
|
|
22
30
|
}
|
|
23
31
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const failure = lastAuthFailure();
|
|
34
|
-
emitFail(command, json, failure.code, failure.message);
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
const summary = await getProject(baseUrl, auth.accessToken, project.projectId);
|
|
38
|
-
return { projectId: project.projectId, harness: summary.harness ?? null };
|
|
32
|
+
function contractView(contract) {
|
|
33
|
+
return {
|
|
34
|
+
projectId: contract.projectId,
|
|
35
|
+
harness: contract.harness,
|
|
36
|
+
origin: contract.origin,
|
|
37
|
+
door: contract.door ?? null,
|
|
38
|
+
docsRoot: contract.docsRoot,
|
|
39
|
+
branchPolicy: contract.branchPolicy,
|
|
40
|
+
};
|
|
39
41
|
}
|
|
40
42
|
export async function runHarnessMount(target, options, globals) {
|
|
41
43
|
const json = options.json === true;
|
|
42
44
|
const root = rootOf(path.resolve(target ?? process.cwd()));
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (!cloud)
|
|
48
|
-
return;
|
|
49
|
-
projectId = cloud.projectId;
|
|
50
|
-
manifestUrl = cloud.harness?.manifestUrl ?? null;
|
|
51
|
-
if (!manifestUrl) {
|
|
52
|
-
emitFail("harness mount", json, "HARNESS_UNAVAILABLE", `Project ${cloud.projectId} resolves no harness version with an uploaded manifest.`);
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
45
|
+
const runtime = options.runtime ?? "claude-code";
|
|
46
|
+
if (!RUNTIMES.includes(runtime)) {
|
|
47
|
+
emitFail("harness mount", json, "VALIDATION_FAILED", `--runtime must be one of ${RUNTIMES.join(", ")}.`);
|
|
48
|
+
return;
|
|
55
49
|
}
|
|
56
|
-
|
|
50
|
+
const writeContract = options.writeContract ?? "never";
|
|
51
|
+
if (!WRITE_CONTRACT_MODES.includes(writeContract)) {
|
|
52
|
+
emitFail("harness mount", json, "VALIDATION_FAILED", `--write-contract must be one of ${WRITE_CONTRACT_MODES.join(", ")}.`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
let outcome;
|
|
57
56
|
try {
|
|
58
|
-
|
|
57
|
+
outcome = await mountProjectHarness(root, {
|
|
58
|
+
globals,
|
|
59
|
+
projectId: options.project,
|
|
60
|
+
runtime: runtime,
|
|
61
|
+
writeContract: writeContract,
|
|
62
|
+
log: (line) => console.error(line),
|
|
63
|
+
});
|
|
59
64
|
}
|
|
60
65
|
catch (error) {
|
|
61
|
-
emitFail("harness mount", json,
|
|
66
|
+
emitFail("harness mount", json, codeOf(error), error instanceof Error ? error.message : String(error));
|
|
62
67
|
return;
|
|
63
68
|
}
|
|
69
|
+
const trailerHook = await installCommitTrailerHook(root);
|
|
70
|
+
const trailerLine = trailerHookLine(trailerHook);
|
|
71
|
+
if (trailerLine && trailerHook !== "skipped_not_git")
|
|
72
|
+
console.error(trailerLine);
|
|
64
73
|
if (json) {
|
|
65
|
-
|
|
74
|
+
const { lines: _lines, ...mounted } = outcome;
|
|
75
|
+
printJsonSuccess("harness mount", { ...mounted, root, trailerHook });
|
|
66
76
|
return;
|
|
67
77
|
}
|
|
68
|
-
console.log(`Mounted harness ${
|
|
78
|
+
console.log(`Mounted harness ${outcome.version ?? "(unknown)"} for ${outcome.runtime} into ${root} from ${outcome.source} ` +
|
|
79
|
+
`(${outcome.written.length} written, ${outcome.kept.length} unchanged, ${outcome.removed.length} removed${outcome.digest ? `, digest ${outcome.digest.slice(0, 12)}` : ""})`);
|
|
69
80
|
}
|
|
70
81
|
export async function runHarnessStatus(options, globals) {
|
|
71
82
|
const json = options.json === true;
|
|
72
83
|
const root = rootOf(process.cwd());
|
|
73
|
-
const
|
|
74
|
-
|
|
84
|
+
const contract = await readProjectContract(root);
|
|
85
|
+
let projectId;
|
|
86
|
+
try {
|
|
87
|
+
projectId = await projectOf(root, options.project, contract.projectId);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
emitFail("harness status", json, codeOf(error), error instanceof Error ? error.message : String(error));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const pinned = contract.harness && "digest" in contract.harness ? contract.harness : null;
|
|
94
|
+
let reading;
|
|
95
|
+
try {
|
|
96
|
+
const read = await readSummary(root, projectId, globals);
|
|
97
|
+
if (read.kind === "offline") {
|
|
98
|
+
if (read.guardRefusal) {
|
|
99
|
+
emitFail("harness status", json, read.guardRefusal.code, read.guardRefusal.message);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
reading = pinned
|
|
103
|
+
? { expected: { version: pinned.pin, digest: pinned.digest }, source: "version", unreachable: null }
|
|
104
|
+
: { expected: null, source: "version", unreachable: read.reason };
|
|
105
|
+
}
|
|
106
|
+
else if (!isProjectVersionSummary(read.summary)) {
|
|
107
|
+
const version = read.summary.harness?.version ?? null;
|
|
108
|
+
if (version === null) {
|
|
109
|
+
emitFail("harness status", json, HARNESS_CODES.HARNESS_UNAVAILABLE, `Project ${projectId} resolves no harness version.`);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
reading = { expected: { version, digest: null }, source: "legacy", unreachable: null };
|
|
113
|
+
}
|
|
114
|
+
else if (pinned) {
|
|
115
|
+
reading = { expected: { version: pinned.pin, digest: pinned.digest }, source: "version", unreachable: null };
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
try {
|
|
119
|
+
const resolved = await resolveProjectVersion(read.baseUrl, read.accessToken, projectId, { track: "latest" });
|
|
120
|
+
reading = { expected: { version: resolved.semver, digest: resolved.digest }, source: "version", unreachable: null };
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
if (codeOf(error) === HARNESS_CODES.ORIGIN_MISMATCH) {
|
|
124
|
+
emitFail("harness status", json, HARNESS_CODES.ORIGIN_MISMATCH, error instanceof Error ? error.message : String(error));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
reading = { expected: null, source: "version", unreachable: error instanceof Error ? error.message : String(error) };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
emitFail("harness status", json, codeOf(error), error instanceof Error ? error.message : String(error));
|
|
75
133
|
return;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
134
|
+
}
|
|
135
|
+
let mounted;
|
|
136
|
+
if (reading.source === "legacy") {
|
|
137
|
+
const legacy = await readLegacyMountRecord(root);
|
|
138
|
+
mounted = legacy ? { version: legacy.version, digest: legacy.digest, mountedAt: legacy.mountedAt, source: "legacy", origin: legacy.origin } : null;
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
mounted = await readMountState(root);
|
|
142
|
+
if (mounted?.source === "legacy")
|
|
143
|
+
mounted = null;
|
|
144
|
+
}
|
|
145
|
+
const inStep = reading.expected === null
|
|
146
|
+
? null
|
|
147
|
+
: reading.source === "legacy"
|
|
148
|
+
? mounted !== null && mounted.version === reading.expected.version
|
|
149
|
+
: mounted !== null && mounted.digest === reading.expected.digest;
|
|
150
|
+
const exit = inStep === null ? STATUS_EXIT.UNREACHABLE : inStep ? STATUS_EXIT.IN_STEP : STATUS_EXIT.DIFFERING;
|
|
79
151
|
if (json) {
|
|
80
|
-
printJsonSuccess("harness status", {
|
|
152
|
+
printJsonSuccess("harness status", {
|
|
153
|
+
projectId,
|
|
154
|
+
contract: contractView(contract),
|
|
155
|
+
mounted,
|
|
156
|
+
expected: reading.expected,
|
|
157
|
+
inStep,
|
|
158
|
+
source: reading.source,
|
|
159
|
+
...(reading.unreachable ? { unreachable: reading.unreachable } : {}),
|
|
160
|
+
});
|
|
81
161
|
}
|
|
82
162
|
else {
|
|
83
|
-
console.log(`Harness for Project ${
|
|
84
|
-
console.log(` mounted:
|
|
85
|
-
console.log(`
|
|
86
|
-
console.log(` verdict:
|
|
163
|
+
console.log(`Harness for Project ${projectId} (${reading.source === "legacy" ? "legacy harness" : "project version"})`);
|
|
164
|
+
console.log(` mounted: ${mounted ? `${mounted.version} (digest ${mounted.digest.slice(0, 12)}, ${mounted.source ?? "unknown source"})` : "(nothing mounted)"}`);
|
|
165
|
+
console.log(` expected: ${reading.expected ? `${reading.expected.version ?? "?"}${reading.expected.digest ? ` (digest ${reading.expected.digest.slice(0, 12)})` : ""}` : `(unreachable: ${reading.unreachable ?? "unknown"})`}`);
|
|
166
|
+
console.log(` verdict: ${inStep === null ? "UNKNOWN — the expected version could not be read" : inStep ? "in step" : "DIFFERS — run `nustack harness mount`"}`);
|
|
87
167
|
}
|
|
88
|
-
|
|
89
|
-
process.exitCode = 1;
|
|
168
|
+
process.exitCode = exit;
|
|
90
169
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import * as p from "@clack/prompts";
|
|
4
4
|
import { createProjectViaAsk, createOrganization, getProject, listProjects, listOrganizations, resolveNuStackUrl, } from "../lib/nustack_client.js";
|
|
5
|
-
import { ensureAccessToken } from "../lib/auth_grant.js";
|
|
5
|
+
import { ensureAccessToken, envToken } from "../lib/auth_grant.js";
|
|
6
6
|
import { getOrCreateDeviceId } from "../lib/device_identity.js";
|
|
7
7
|
import { isInteractive } from "../lib/interactive.js";
|
|
8
8
|
import { resolveOrganizationId } from "../lib/project_context.js";
|
|
@@ -15,9 +15,28 @@ import { attachExistingGit } from "../lib/onboarding/attach_existing_git.js";
|
|
|
15
15
|
import { initFreshFolder } from "../lib/onboarding/init_fresh_folder.js";
|
|
16
16
|
import { webFirstAttach } from "../lib/onboarding/web_first_attach.js";
|
|
17
17
|
import { performDeviceLogin } from "./login.js";
|
|
18
|
+
import { printJsonError, printJsonSuccess } from "../lib/json_output.js";
|
|
19
|
+
import { readProjectContract } from "../lib/project_contract.js";
|
|
20
|
+
import { HARNESS_CODES } from "../lib/harness/codes.js";
|
|
21
|
+
import { nullUnlessOriginRefused } from "../lib/origin_guard.js";
|
|
18
22
|
export async function runInit(options) {
|
|
19
23
|
const cwd = process.cwd();
|
|
20
24
|
const baseUrl = resolveNuStackUrl(options);
|
|
25
|
+
const isUnattended = options.yes === true && Boolean(options.project?.trim()) && Boolean(options.organization?.trim());
|
|
26
|
+
if (isUnattended) {
|
|
27
|
+
await runUnattendedInit(baseUrl, options);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (options.json || !isInteractive()) {
|
|
31
|
+
const message = "nustack init with no terminal needs `--yes --project <id> --organization <id>` (it never prompts, opens a browser or creates a Project); a credential comes from NUSTACK_TOKEN or `nustack login`.";
|
|
32
|
+
if (options.json)
|
|
33
|
+
printJsonError("init", { code: HARNESS_CODES.INIT_NEEDS_PROJECT, message });
|
|
34
|
+
else {
|
|
35
|
+
console.error(message);
|
|
36
|
+
process.exitCode = 1;
|
|
37
|
+
}
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
21
40
|
p.intro("nustack init");
|
|
22
41
|
if (!isInteractive()) {
|
|
23
42
|
p.log.error("nustack init needs an interactive terminal (browser sign-in + prompts).");
|
|
@@ -34,8 +53,87 @@ export async function runInit(options) {
|
|
|
34
53
|
organization: options.organization,
|
|
35
54
|
});
|
|
36
55
|
}
|
|
56
|
+
function initEnvelope(result) {
|
|
57
|
+
return {
|
|
58
|
+
projectId: result.projectId,
|
|
59
|
+
organizationId: result.organizationId,
|
|
60
|
+
contract: result.projectManifest,
|
|
61
|
+
sessionHook: result.sessionHook,
|
|
62
|
+
mount: result.mount,
|
|
63
|
+
trailerHook: result.trailerHook,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
async function runUnattendedInit(baseUrl, options) {
|
|
67
|
+
const json = options.json === true;
|
|
68
|
+
const projectId = options.project.trim();
|
|
69
|
+
const organizationId = options.organization.trim();
|
|
70
|
+
const fail = (code, message) => {
|
|
71
|
+
if (json)
|
|
72
|
+
printJsonError("init", { code, message });
|
|
73
|
+
else {
|
|
74
|
+
console.error(message);
|
|
75
|
+
process.exitCode = 1;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
if (envToken()?.kind === "ok") {
|
|
79
|
+
fail(HARNESS_CODES.INIT_NEEDS_DEVICE, "An unattended init binds this folder to the Project as this machine, which takes a device sign-in: run `nustack login` (or `nustack login --manual`) once, and unset NUSTACK_TOKEN for the init.");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const auth = await ensureAccessToken(baseUrl).catch(nullUnlessOriginRefused);
|
|
83
|
+
if (!auth) {
|
|
84
|
+
fail("SIGNED_OUT", "This machine is not signed in: run `nustack login` (or `nustack login --manual`) once first. An unattended init never opens a browser, and a NUSTACK_TOKEN cannot bind a folder.");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const cwd = process.cwd();
|
|
88
|
+
const bound = (await readProjectContract(cwd)).projectId;
|
|
89
|
+
if (bound && bound !== projectId) {
|
|
90
|
+
fail("REBIND_CONFIRMATION_REQUIRED", `This folder is bound to Project ${bound}. An unattended init never rebinds it to ${projectId}; run \`nustack init\` in a terminal to confirm a rebind.`);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const store = await openLocalStore();
|
|
94
|
+
try {
|
|
95
|
+
const device = readDeviceState(store);
|
|
96
|
+
const ctx = {
|
|
97
|
+
store,
|
|
98
|
+
baseUrl,
|
|
99
|
+
accessToken: auth.accessToken,
|
|
100
|
+
deviceRef: device?.deviceRef ?? (await getOrCreateDeviceId()),
|
|
101
|
+
userId: device?.activeUserId ?? null,
|
|
102
|
+
cwd,
|
|
103
|
+
json,
|
|
104
|
+
yes: true,
|
|
105
|
+
interactive: false,
|
|
106
|
+
};
|
|
107
|
+
const state = classifyFolder(cwd);
|
|
108
|
+
let outcome;
|
|
109
|
+
if (state.class === "git_worktree" && state.repository && state.git) {
|
|
110
|
+
const root = state.repository.repositoryRoot;
|
|
111
|
+
if (!hasManifest(root))
|
|
112
|
+
await scaffoldManifest(root, deriveRepoName(path.basename(root)));
|
|
113
|
+
outcome = await attachExistingGit(ctx, { projectId, organizationId, confirmRebind: false });
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
outcome = await webFirstAttach(ctx, { projectId, organizationId, confirmPopulated: true });
|
|
117
|
+
}
|
|
118
|
+
if (!outcome.ok) {
|
|
119
|
+
fail(outcome.code, outcome.message);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (json) {
|
|
123
|
+
printJsonSuccess("init", initEnvelope(outcome.result));
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
for (const notice of outcome.result.notices)
|
|
127
|
+
console.error(notice);
|
|
128
|
+
console.log(`Bound to Project ${outcome.result.projectId} on branch ${outcome.result.boundBranch} (binding ${outcome.result.bindingId}).`);
|
|
129
|
+
console.log(`Harness: ${outcome.result.mount.status}${outcome.result.mount.version ? ` ${outcome.result.mount.version}` : ""} · session hook ${outcome.result.sessionHook} · trailer hook ${outcome.result.trailerHook}.`);
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
store.close();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
37
135
|
export async function ensureSignedIn(baseUrl, cwd) {
|
|
38
|
-
const deviceAuth = await ensureAccessToken().catch(
|
|
136
|
+
const deviceAuth = await ensureAccessToken().catch(nullUnlessOriginRefused);
|
|
39
137
|
if (deviceAuth) {
|
|
40
138
|
p.log.success(`Signed in as ${deviceAuth.userEmail}`);
|
|
41
139
|
return deviceAuth.accessToken;
|
|
@@ -45,7 +143,7 @@ export async function ensureSignedIn(baseUrl, cwd) {
|
|
|
45
143
|
process.exitCode = 1;
|
|
46
144
|
return null;
|
|
47
145
|
}
|
|
48
|
-
const refreshed = await ensureAccessToken().catch(
|
|
146
|
+
const refreshed = await ensureAccessToken().catch(nullUnlessOriginRefused);
|
|
49
147
|
if (!refreshed) {
|
|
50
148
|
p.outro("Sign-in did not complete — run `nustack login` and retry.");
|
|
51
149
|
process.exitCode = 1;
|
|
@@ -253,6 +351,8 @@ async function confirm(message) {
|
|
|
253
351
|
return !p.isCancel(answer) && answer === true;
|
|
254
352
|
}
|
|
255
353
|
function reportSuccess(result) {
|
|
354
|
+
for (const notice of result.notices)
|
|
355
|
+
p.log.warn(notice);
|
|
256
356
|
p.log.success(`Bound to Project ${result.projectId} on branch ${result.boundBranch} (binding ${result.bindingId}).`);
|
|
257
357
|
p.log.info(`Toolkit ${result.toolkit.name}@${result.toolkit.version}. No Git push, pull, or remote was created.`);
|
|
258
358
|
p.outro("Ready — `nustack status` · `nustack docs list` · `nustack project sync now`.");
|
package/dist/commands/login.js
CHANGED
|
@@ -3,7 +3,8 @@ import { resolveNuStackUrl } from "../lib/nustack_client.js";
|
|
|
3
3
|
import { launchBrowser } from "../lib/browser_opener.js";
|
|
4
4
|
import { selectCredentialStore } from "../lib/credential_store.js";
|
|
5
5
|
import { isInteractive } from "../lib/interactive.js";
|
|
6
|
-
import { ensureAccessToken, ENV_TOKEN_VARIABLE, envToken, loginWithLoopback, loginWithPastedCode, prepareLogin, } from "../lib/auth_grant.js";
|
|
6
|
+
import { ensureAccessToken, ENV_TOKEN_VARIABLE, envToken, loginWithLoopback, loginWithPastedCode, prepareLogin, readStoredCredential, } from "../lib/auth_grant.js";
|
|
7
|
+
import { isContractOrigin, nullUnlessOriginRefused, originMismatchError, refusedContractOrigin } from "../lib/origin_guard.js";
|
|
7
8
|
async function readStdinLine() {
|
|
8
9
|
return new Promise((resolve) => {
|
|
9
10
|
let buffered = "";
|
|
@@ -77,6 +78,12 @@ async function loopbackFlow(ctx, flags) {
|
|
|
77
78
|
}
|
|
78
79
|
}
|
|
79
80
|
export async function performDeviceLogin(baseUrl, _cwd = process.cwd(), flags = {}) {
|
|
81
|
+
if (isContractOrigin(baseUrl)) {
|
|
82
|
+
const held = envToken() ? null : (await readStoredCredential().catch(() => null))?.credential ?? null;
|
|
83
|
+
const refused = refusedContractOrigin(held?.studioBaseUrl ?? null, baseUrl);
|
|
84
|
+
if (refused)
|
|
85
|
+
throw originMismatchError(refused, held?.studioBaseUrl ?? null, envToken() ? "pat" : held ? "device" : "none");
|
|
86
|
+
}
|
|
80
87
|
if (envToken()) {
|
|
81
88
|
p.log.info(`${ENV_TOKEN_VARIABLE} is set — this process authenticates with it, and no sign-in is stored on this machine. Unset it to sign in as a person.`);
|
|
82
89
|
return true;
|
|
@@ -90,7 +97,7 @@ export async function performDeviceLogin(baseUrl, _cwd = process.cwd(), flags =
|
|
|
90
97
|
return false;
|
|
91
98
|
}
|
|
92
99
|
if (!flags.force && !flags.reauthorize) {
|
|
93
|
-
const live = await ensureAccessToken().catch(
|
|
100
|
+
const live = await ensureAccessToken().catch(nullUnlessOriginRefused);
|
|
94
101
|
if (live) {
|
|
95
102
|
p.log.success(`Signed in as ${live.userEmail || "this account"} — already signed in on this machine (use --force to sign in again).`);
|
|
96
103
|
return true;
|
package/dist/commands/mcp.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveAccessToken } from "../lib/auth_grant.js";
|
|
1
2
|
import { ensureAuthWithRecovery } from "../lib/auth_recovery.js";
|
|
2
3
|
import { printJsonError, printJsonSuccess } from "../lib/json_output.js";
|
|
3
4
|
import { authorizedFetch, resolveNuStackUrl } from "../lib/nustack_client.js";
|
|
@@ -10,6 +11,12 @@ async function runServe(options, globals) {
|
|
|
10
11
|
const bound = flag ? null : await resolveProjectId(process.cwd(), {});
|
|
11
12
|
const defaultProjectId = flag ?? (bound?.ok ? bound.projectId : null);
|
|
12
13
|
const doorUrl = doorUrlFor(baseUrl, process.env.NUSTACK_MCP_URL);
|
|
14
|
+
const outcome = await resolveAccessToken(baseUrl).catch(() => null);
|
|
15
|
+
if (outcome?.kind === "origin_mismatch") {
|
|
16
|
+
process.stderr.write(`nustack mcp: ${outcome.error.message}\n`);
|
|
17
|
+
process.exitCode = 1;
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
13
20
|
process.stderr.write(`nustack mcp: relaying to ${doorUrl}${defaultProjectId ? `, default project ${defaultProjectId}` : ""}\n`);
|
|
14
21
|
await runRelay({
|
|
15
22
|
doorUrl,
|
|
@@ -12,6 +12,7 @@ import { listBundles, restoreBundle, deleteBundle, sweepExpiredBundles, } from "
|
|
|
12
12
|
import { resolveRepository } from "../lib/git-sync/core/repository.js";
|
|
13
13
|
import { registerOutbound, createUploadClient, deviceTokenProvider, describeHistoryDecision, publishCurrentState, adoptWithoutPublishing, } from "../lib/git-sync/outbound/index.js";
|
|
14
14
|
import { latestAcceptedUpload } from "../lib/local_store/accepted_uploads.js";
|
|
15
|
+
import { nullUnlessOriginRefused } from "../lib/origin_guard.js";
|
|
15
16
|
const ONBOARDING_POINTER = "No NuStack Project bindings on this machine. Run `nustack project attach` inside a Git repository to bind it.";
|
|
16
17
|
function writeLine(line) {
|
|
17
18
|
process.stdout.write(`${line}\n`);
|
|
@@ -92,7 +93,7 @@ export function registerProjectSyncCommands(projectCommand, getGlobalOptions) {
|
|
|
92
93
|
}
|
|
93
94
|
async function buildInboundRuntime(store, globals) {
|
|
94
95
|
const baseUrl = resolveNuStackUrl(globals);
|
|
95
|
-
const auth = await ensureAccessToken().catch(
|
|
96
|
+
const auth = await ensureAccessToken().catch(nullUnlessOriginRefused);
|
|
96
97
|
if (!auth)
|
|
97
98
|
return null;
|
|
98
99
|
const deviceState = readDeviceState(store);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { InvalidArgumentError } from "commander";
|
|
2
|
+
import { runReadBulk as runRelay } from "../lib/gate/bulkReadRelay.js";
|
|
3
|
+
import { isReadBulkBudgetInRange, READ_BULK_BUDGET_BOUNDS_MS } from "../lib/gate/settings.js";
|
|
4
|
+
export function parseBudgetMs(value) {
|
|
5
|
+
const parsed = /^\d+$/.test(value.trim()) ? Number.parseInt(value.trim(), 10) : Number.NaN;
|
|
6
|
+
if (!isReadBulkBudgetInRange(parsed)) {
|
|
7
|
+
throw new InvalidArgumentError(`the budget is an integer from ${READ_BULK_BUDGET_BOUNDS_MS.min} to ${READ_BULK_BUDGET_BOUNDS_MS.max} milliseconds.`);
|
|
8
|
+
}
|
|
9
|
+
return parsed;
|
|
10
|
+
}
|
|
11
|
+
export function parseLineWindow(value) {
|
|
12
|
+
if (!/^\d+$/.test(value.trim()) || Number.parseInt(value.trim(), 10) < 1) {
|
|
13
|
+
throw new InvalidArgumentError("the line window is a positive integer.");
|
|
14
|
+
}
|
|
15
|
+
return Number.parseInt(value.trim(), 10);
|
|
16
|
+
}
|
|
17
|
+
export async function runReadBulk(options) {
|
|
18
|
+
await runRelay(options);
|
|
19
|
+
}
|
package/dist/commands/records.js
CHANGED
|
@@ -3,6 +3,7 @@ import { readDesktopSettings } from "../lib/desktop_settings.js";
|
|
|
3
3
|
import { openLocalStore } from "../lib/local_store/database.js";
|
|
4
4
|
import { stateDbPath } from "../lib/local_store/paths.js";
|
|
5
5
|
import { printJsonError, printJsonSuccess } from "../lib/json_output.js";
|
|
6
|
+
import { HARNESS_CODES } from "../lib/harness/codes.js";
|
|
6
7
|
import { resolveProjectId } from "../lib/project_context.js";
|
|
7
8
|
import { isEmitKind } from "../lib/records/door.js";
|
|
8
9
|
import { acquireDrainLock, drainRecords, refreshDrainLock, releaseDrainLock, startDetachedDrain } from "../lib/records/drain.js";
|
|
@@ -177,6 +178,15 @@ export async function runRecordsSync(options, globals) {
|
|
|
177
178
|
if (project.ok)
|
|
178
179
|
releaseUnboundRows(store, root);
|
|
179
180
|
const drained = await drainAs(store, globals, isFlagSet(DRAIN_CHILD_VARIABLE));
|
|
181
|
+
if (drained?.originRefusal) {
|
|
182
|
+
if (options.json)
|
|
183
|
+
printJsonError("records sync", { code: HARNESS_CODES.ORIGIN_MISMATCH, message: drained.originRefusal });
|
|
184
|
+
else {
|
|
185
|
+
process.stderr.write(`${drained.originRefusal}\n`);
|
|
186
|
+
process.exitCode = 1;
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
180
190
|
if (options.quiet)
|
|
181
191
|
return;
|
|
182
192
|
const counts = await countRecords(store);
|