aidimag 1.0.10 → 1.0.12
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/cli/commands/sync.js +71 -20
- package/dist/cli/commands/tickets.js +3 -3
- package/dist/sync/client.d.ts +5 -7
- package/dist/sync/client.js +23 -44
- package/dist/tickets/provider.js +1 -3
- package/dist/ui/server.js +20 -9
- package/package.json +1 -1
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Team-sync commands: serve, cloud, login, logout, sync, keys.
|
|
3
3
|
*/
|
|
4
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
4
5
|
import { MemoryStore, findRepoRoot } from "../../db/store.js";
|
|
5
6
|
import { fail, maybeRegenerateContext, openBrowser, createPrompter } from "../shared.js";
|
|
7
|
+
import { configPath } from "../../sync/client.js";
|
|
6
8
|
export function registerSyncCommands(program) {
|
|
7
9
|
program
|
|
8
10
|
.command("serve")
|
|
@@ -25,7 +27,7 @@ export function registerSyncCommands(program) {
|
|
|
25
27
|
.argument("<action>", "link | unlink | status | remote")
|
|
26
28
|
.option("-s, --server <url>", "Sync server URL")
|
|
27
29
|
.option("-b, --brain <name>", "Brain (team memory) name on the server")
|
|
28
|
-
.option("-t, --token <token>", "Auth token (stored in
|
|
30
|
+
.option("-t, --token <token>", "Auth token (stored in .aidimag/config.json)")
|
|
29
31
|
.option("--json", "Machine-readable output (remote)")
|
|
30
32
|
.option("--id <memoryId>", "Show one remote memory by id (remote)")
|
|
31
33
|
.option("--limit <n>", "Max rows to list (remote)", "20")
|
|
@@ -35,19 +37,34 @@ export function registerSyncCommands(program) {
|
|
|
35
37
|
.option("--full", "Include full remote payload JSON (remote)")
|
|
36
38
|
.action(async (action, opts) => {
|
|
37
39
|
const root = findRepoRoot() ?? fail("not inside a repo");
|
|
38
|
-
const { readCloudConfig, writeCloudConfig,
|
|
40
|
+
const { readCloudConfig, writeCloudConfig, getToken, fetchRemoteSnapshot, syncMetaKey } = await import("../../sync/client.js");
|
|
39
41
|
switch (action) {
|
|
40
42
|
case "link": {
|
|
41
43
|
if (!opts.server || !opts.brain)
|
|
42
44
|
fail("usage: dim cloud link --server <url> --brain <name> [--token <token>]");
|
|
43
45
|
const server = String(opts.server).replace(/\/$/, "");
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
// Always store token in project config
|
|
47
|
+
const p = configPath(root);
|
|
48
|
+
let existing = {};
|
|
49
|
+
try {
|
|
50
|
+
existing = JSON.parse(readFileSync(p, "utf8"));
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// fresh file
|
|
54
|
+
}
|
|
55
|
+
const newConfig = { ...existing, server, brain: opts.brain };
|
|
56
|
+
if (opts.token) {
|
|
57
|
+
newConfig.token = opts.token;
|
|
58
|
+
}
|
|
59
|
+
writeFileSync(p, JSON.stringify(newConfig, null, 2) + "\n");
|
|
47
60
|
console.log(`Linked to ${server} (brain: ${opts.brain}).`);
|
|
48
|
-
console.log(`Config in .aidimag/config.json (
|
|
49
|
-
if (!opts.token && !getToken(server)) {
|
|
50
|
-
console.log("⚠ No token stored yet — pass --token or set AIDIMAG_API_KEY before
|
|
61
|
+
console.log(`Config stored in .aidimag/config.json (per-project).`);
|
|
62
|
+
if (!opts.token && !getToken(server, root)) {
|
|
63
|
+
console.log("⚠ No token stored yet — pass --token or set AIDIMAG_API_KEY before \`dim sync\`.");
|
|
64
|
+
}
|
|
65
|
+
else if (opts.token) {
|
|
66
|
+
console.log(`✓ Token stored in .aidimag/config.json`);
|
|
67
|
+
console.log(`⚠ Add .aidimag/config.json to .gitignore if you don't want to commit the token.`);
|
|
51
68
|
}
|
|
52
69
|
break;
|
|
53
70
|
}
|
|
@@ -60,8 +77,10 @@ export function registerSyncCommands(program) {
|
|
|
60
77
|
const cfg = readCloudConfig(root);
|
|
61
78
|
if (!cfg)
|
|
62
79
|
console.log("Not cloud-linked. Use `dim cloud link`.");
|
|
63
|
-
else
|
|
64
|
-
|
|
80
|
+
else {
|
|
81
|
+
const token = getToken(cfg.server, root);
|
|
82
|
+
console.log(`server: ${cfg.server}\nbrain: ${cfg.brain}\ntoken: ${token ? "stored in .aidimag/config.json" : "MISSING"}`);
|
|
83
|
+
}
|
|
65
84
|
break;
|
|
66
85
|
}
|
|
67
86
|
case "remote": {
|
|
@@ -146,7 +165,9 @@ export function registerSyncCommands(program) {
|
|
|
146
165
|
.action(async (opts) => {
|
|
147
166
|
const { readCloudConfig, startDeviceLogin, pollDeviceLogin } = await import("../../sync/client.js");
|
|
148
167
|
const root = findRepoRoot();
|
|
149
|
-
|
|
168
|
+
if (!root)
|
|
169
|
+
fail("not inside a repo — run this command from a project directory");
|
|
170
|
+
const server = opts.server?.replace(/\/$/, "") ?? readCloudConfig(root)?.server;
|
|
150
171
|
if (!server)
|
|
151
172
|
fail("no server: pass --server <url> or link the repo with `dim cloud link` first");
|
|
152
173
|
const start = await startDeviceLogin(server);
|
|
@@ -155,20 +176,50 @@ export function registerSyncCommands(program) {
|
|
|
155
176
|
if (opts.open)
|
|
156
177
|
await openBrowser(approveUrl);
|
|
157
178
|
console.log("Waiting for approval…");
|
|
158
|
-
const { brain } = await pollDeviceLogin(server, start);
|
|
159
|
-
|
|
179
|
+
const { token, brain } = await pollDeviceLogin(server, start);
|
|
180
|
+
// Save token to project config
|
|
181
|
+
const p = configPath(root);
|
|
182
|
+
let existing = {};
|
|
183
|
+
try {
|
|
184
|
+
existing = JSON.parse(readFileSync(p, "utf8"));
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
// fresh file
|
|
188
|
+
}
|
|
189
|
+
existing.token = token;
|
|
190
|
+
writeFileSync(p, JSON.stringify(existing, null, 2) + "\n");
|
|
191
|
+
console.log(`✓ Logged in to ${server} (scope: ${brain ?? "all brains"}).`);
|
|
192
|
+
console.log(`✓ Token saved to .aidimag/config.json (per-project).`);
|
|
193
|
+
console.log(`⚠ Add .aidimag/config.json to .gitignore if you don't want to commit the token.`);
|
|
160
194
|
});
|
|
161
195
|
program
|
|
162
196
|
.command("logout")
|
|
163
|
-
.description("Remove this device's stored token
|
|
164
|
-
.option("-s, --server <url>", "Server URL (defaults to the repo's linked server)")
|
|
197
|
+
.description("Remove this device's stored token from the project config")
|
|
165
198
|
.action(async (opts) => {
|
|
166
|
-
const { readCloudConfig
|
|
199
|
+
const { readCloudConfig } = await import("../../sync/client.js");
|
|
167
200
|
const root = findRepoRoot();
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
201
|
+
if (!root)
|
|
202
|
+
fail("not inside a repo — run this command from a project directory");
|
|
203
|
+
const cfg = readCloudConfig(root);
|
|
204
|
+
if (!cfg)
|
|
205
|
+
fail("not cloud-linked. Use `dim cloud link` first");
|
|
206
|
+
// Remove token from project config
|
|
207
|
+
const p = configPath(root);
|
|
208
|
+
let existing = {};
|
|
209
|
+
try {
|
|
210
|
+
existing = JSON.parse(readFileSync(p, "utf8"));
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
console.log("No config file found.");
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (!existing.token) {
|
|
217
|
+
console.log("No token stored in this project.");
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
delete existing.token;
|
|
221
|
+
writeFileSync(p, JSON.stringify(existing, null, 2) + "\n");
|
|
222
|
+
console.log(`✓ Logged out — token removed from .aidimag/config.json`);
|
|
172
223
|
});
|
|
173
224
|
program
|
|
174
225
|
.command("sync")
|
|
@@ -61,12 +61,12 @@ export function registerTicketCommands(program) {
|
|
|
61
61
|
});
|
|
62
62
|
console.log(`🎫 Connected via the team sync server (${cloud.server}, brain: ${cloud.brain}).`);
|
|
63
63
|
console.log(` Zero local ticket credentials — the server holds the team token.`);
|
|
64
|
-
if (!getToken(cloud.server))
|
|
64
|
+
if (!getToken(cloud.server, root))
|
|
65
65
|
console.log(` ⚠ No sync token on this machine yet — run \`dim login\` first.`);
|
|
66
66
|
// trust-building: check the server actually has a team config
|
|
67
67
|
try {
|
|
68
68
|
const res = await fetch(`${cloud.server}/v1/ticket-config?brain=${encodeURIComponent(cloud.brain)}`, {
|
|
69
|
-
headers: { Authorization: `Bearer ${getToken(cloud.server) ?? ""}` },
|
|
69
|
+
headers: { Authorization: `Bearer ${getToken(cloud.server, root) ?? ""}` },
|
|
70
70
|
});
|
|
71
71
|
const body = (await res.json());
|
|
72
72
|
if (res.ok && body.config?.provider) {
|
|
@@ -150,7 +150,7 @@ export function registerTicketCommands(program) {
|
|
|
150
150
|
if (cfg.provider === "remote") {
|
|
151
151
|
const { readCloudConfig, getToken } = await import("../../sync/client.js");
|
|
152
152
|
const cloud = readCloudConfig(root);
|
|
153
|
-
console.log(`provider: remote (via the team sync server)\nserver: ${cloud?.server ?? "NOT LINKED — dim cloud link"}\nbrain: ${cloud?.brain ?? "—"}\npattern: ${cfg.pattern ?? tickets.DEFAULT_TICKET_PATTERN}\ntoken: ${cloud && getToken(cloud.server) ? "sync token stored" : "MISSING — dim login"}`);
|
|
153
|
+
console.log(`provider: remote (via the team sync server)\nserver: ${cloud?.server ?? "NOT LINKED — dim cloud link"}\nbrain: ${cloud?.brain ?? "—"}\npattern: ${cfg.pattern ?? tickets.DEFAULT_TICKET_PATTERN}\ntoken: ${cloud && getToken(cloud.server, root) ? "sync token stored" : "MISSING — dim login"}`);
|
|
154
154
|
}
|
|
155
155
|
else {
|
|
156
156
|
const credKey = cfg.baseUrl ?? "linear";
|
package/dist/sync/client.d.ts
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* push: rows changed since last push (+ tombstones) → server
|
|
6
6
|
* pull: latest remote rows since cursor → apply if remote.updatedAt > local
|
|
7
7
|
*
|
|
8
|
-
* Config
|
|
9
|
-
* .aidimag/config.json
|
|
10
|
-
*
|
|
8
|
+
* Config (per-project):
|
|
9
|
+
* .aidimag/config.json { server, brain, token } — per-project, add to .gitignore if token is sensitive
|
|
10
|
+
* AIDIMAG_API_KEY env var — alternative to storing token in config
|
|
11
11
|
*/
|
|
12
12
|
import type { MemoryStore } from "../db/store.js";
|
|
13
13
|
import type { RemoteSnapshot } from "./server.js";
|
|
@@ -20,9 +20,7 @@ export declare function syncMetaKey(base: string, brain: string): string;
|
|
|
20
20
|
export declare function configPath(repoRoot: string): string;
|
|
21
21
|
export declare function readCloudConfig(repoRoot: string): CloudConfig | null;
|
|
22
22
|
export declare function writeCloudConfig(repoRoot: string, cfg: CloudConfig): void;
|
|
23
|
-
export declare function getToken(server: string): string | null;
|
|
24
|
-
export declare function saveToken(server: string, token: string): void;
|
|
25
|
-
export declare function removeToken(server: string): boolean;
|
|
23
|
+
export declare function getToken(server: string, repoRoot?: string): string | null;
|
|
26
24
|
export interface DeviceStart {
|
|
27
25
|
device_code: string;
|
|
28
26
|
user_code: string;
|
|
@@ -32,7 +30,7 @@ export interface DeviceStart {
|
|
|
32
30
|
}
|
|
33
31
|
/** Begin a device-code login: server hands back a user code + approval URL. */
|
|
34
32
|
export declare function startDeviceLogin(server: string): Promise<DeviceStart>;
|
|
35
|
-
/** Poll until the device is approved in the browser.
|
|
33
|
+
/** Poll until the device is approved in the browser. Returns the token (caller must save it). */
|
|
36
34
|
export declare function pollDeviceLogin(server: string, start: DeviceStart): Promise<{
|
|
37
35
|
token: string;
|
|
38
36
|
brain: string | null;
|
package/dist/sync/client.js
CHANGED
|
@@ -5,12 +5,11 @@
|
|
|
5
5
|
* push: rows changed since last push (+ tombstones) → server
|
|
6
6
|
* pull: latest remote rows since cursor → apply if remote.updatedAt > local
|
|
7
7
|
*
|
|
8
|
-
* Config
|
|
9
|
-
* .aidimag/config.json
|
|
10
|
-
*
|
|
8
|
+
* Config (per-project):
|
|
9
|
+
* .aidimag/config.json { server, brain, token } — per-project, add to .gitignore if token is sensitive
|
|
10
|
+
* AIDIMAG_API_KEY env var — alternative to storing token in config
|
|
11
11
|
*/
|
|
12
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync
|
|
13
|
-
import { homedir } from "node:os";
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
14
13
|
import path from "node:path";
|
|
15
14
|
import { debugLog } from "../debug.js";
|
|
16
15
|
const CURSOR_KEY = "sync_pull_cursor";
|
|
@@ -52,43 +51,24 @@ export function writeCloudConfig(repoRoot, cfg) {
|
|
|
52
51
|
const brain = cfg.brain.trim();
|
|
53
52
|
writeFileSync(p, JSON.stringify({ ...existing, server, brain }, null, 2) + "\n");
|
|
54
53
|
}
|
|
55
|
-
function
|
|
56
|
-
return path.join(homedir(), ".aidimag", "credentials.json");
|
|
57
|
-
}
|
|
58
|
-
export function getToken(server) {
|
|
54
|
+
export function getToken(server, repoRoot) {
|
|
59
55
|
if (process.env.AIDIMAG_API_KEY)
|
|
60
56
|
return process.env.AIDIMAG_API_KEY;
|
|
61
|
-
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const creds = existsSync(p) ? JSON.parse(readFileSync(p, "utf8")) : {};
|
|
75
|
-
creds[server] = token;
|
|
76
|
-
writeFileSync(p, JSON.stringify(creds, null, 2) + "\n", { mode: 0o600 });
|
|
77
|
-
try {
|
|
78
|
-
chmodSync(p, 0o600);
|
|
57
|
+
// Always use project-level config
|
|
58
|
+
if (repoRoot) {
|
|
59
|
+
const projectConfigPath = configPath(repoRoot);
|
|
60
|
+
if (existsSync(projectConfigPath)) {
|
|
61
|
+
try {
|
|
62
|
+
const cfg = JSON.parse(readFileSync(projectConfigPath, "utf8"));
|
|
63
|
+
if (cfg.token)
|
|
64
|
+
return cfg.token;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// ignore
|
|
68
|
+
}
|
|
69
|
+
}
|
|
79
70
|
}
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
export function removeToken(server) {
|
|
83
|
-
const p = credentialsPath();
|
|
84
|
-
if (!existsSync(p))
|
|
85
|
-
return false;
|
|
86
|
-
const creds = JSON.parse(readFileSync(p, "utf8"));
|
|
87
|
-
if (!(server in creds))
|
|
88
|
-
return false;
|
|
89
|
-
delete creds[server];
|
|
90
|
-
writeFileSync(p, JSON.stringify(creds, null, 2) + "\n", { mode: 0o600 });
|
|
91
|
-
return true;
|
|
71
|
+
return null;
|
|
92
72
|
}
|
|
93
73
|
/** Begin a device-code login: server hands back a user code + approval URL. */
|
|
94
74
|
export async function startDeviceLogin(server) {
|
|
@@ -100,7 +80,7 @@ export async function startDeviceLogin(server) {
|
|
|
100
80
|
}
|
|
101
81
|
return (await res.json());
|
|
102
82
|
}
|
|
103
|
-
/** Poll until the device is approved in the browser.
|
|
83
|
+
/** Poll until the device is approved in the browser. Returns the token (caller must save it). */
|
|
104
84
|
export async function pollDeviceLogin(server, start) {
|
|
105
85
|
const deadline = Date.now() + start.expires_in * 1000;
|
|
106
86
|
for (;;) {
|
|
@@ -117,7 +97,6 @@ export async function pollDeviceLogin(server, start) {
|
|
|
117
97
|
if (!res.ok)
|
|
118
98
|
throw new Error(`login failed: HTTP ${res.status} ${await res.text()}`);
|
|
119
99
|
const out = (await res.json());
|
|
120
|
-
saveToken(server, out.token);
|
|
121
100
|
return out;
|
|
122
101
|
}
|
|
123
102
|
}
|
|
@@ -162,7 +141,7 @@ export async function fetchRemoteSnapshot(repoRoot, opts = {}) {
|
|
|
162
141
|
const cfg = readCloudConfig(repoRoot);
|
|
163
142
|
if (!cfg)
|
|
164
143
|
throw new Error("repo is not cloud-linked. Run `dim cloud link --server <url> --brain <name> --token <token>` first.");
|
|
165
|
-
const token = getToken(cfg.server);
|
|
144
|
+
const token = getToken(cfg.server, repoRoot);
|
|
166
145
|
if (!token)
|
|
167
146
|
throw new Error(`no credentials for ${cfg.server}. Run \`dim cloud link\` with --token, or set AIDIMAG_API_KEY.`);
|
|
168
147
|
const q = new URLSearchParams({ brain: cfg.brain });
|
|
@@ -203,7 +182,7 @@ export async function sync(store, repoRoot, opts = {}) {
|
|
|
203
182
|
const cfg = readCloudConfig(repoRoot);
|
|
204
183
|
if (!cfg)
|
|
205
184
|
throw new Error("repo is not cloud-linked. Run `dim cloud link --server <url> --brain <name> --token <token>` first.");
|
|
206
|
-
const token = getToken(cfg.server);
|
|
185
|
+
const token = getToken(cfg.server, repoRoot);
|
|
207
186
|
if (!token)
|
|
208
187
|
throw new Error(`no credentials for ${cfg.server}. Run \`dim cloud link\` with --token, or set AIDIMAG_API_KEY.`);
|
|
209
188
|
const cursorKey = syncMetaKey(CURSOR_KEY, cfg.brain);
|
|
@@ -390,7 +369,7 @@ export async function maybeAutoSync(store, repoRoot) {
|
|
|
390
369
|
const cfg = readCloudConfig(repoRoot);
|
|
391
370
|
if (!cfg)
|
|
392
371
|
return null;
|
|
393
|
-
if (!getToken(cfg.server))
|
|
372
|
+
if (!getToken(cfg.server, repoRoot))
|
|
394
373
|
return null;
|
|
395
374
|
const last = store.getMeta(AUTO_SYNC_LAST_KEY);
|
|
396
375
|
if (last && Date.now() - new Date(last).getTime() < AUTO_SYNC_DEBOUNCE_MS)
|
package/dist/tickets/provider.js
CHANGED
|
@@ -321,9 +321,7 @@ export function ticketProviderFor(repoRoot) {
|
|
|
321
321
|
const raw = JSON.parse(readFileSync(p, "utf8"));
|
|
322
322
|
if (!raw.server || !raw.brain)
|
|
323
323
|
return null;
|
|
324
|
-
const token = process.env.AIDIMAG_API_KEY ??
|
|
325
|
-
JSON.parse(readFileSync(credentialsPath(), "utf8"))[raw.server] ??
|
|
326
|
-
null;
|
|
324
|
+
const token = process.env.AIDIMAG_API_KEY ?? raw.token ?? null;
|
|
327
325
|
return token ? new RemoteProvider(raw.server, raw.brain, token) : null;
|
|
328
326
|
}
|
|
329
327
|
catch {
|
package/dist/ui/server.js
CHANGED
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
* same dashboard in a webview.
|
|
8
8
|
*/
|
|
9
9
|
import { createServer } from "node:http";
|
|
10
|
-
import { watch, mkdirSync } from "node:fs";
|
|
10
|
+
import { watch, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
11
|
import path from "node:path";
|
|
12
12
|
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
13
13
|
import { verifyAll } from "../verify/engine.js";
|
|
14
14
|
import { mineCommits } from "../capture/commit-miner.js";
|
|
15
15
|
import { hybridSearch, indexMemory, reindexAll } from "../embeddings/search.js";
|
|
16
|
-
import { readCloudConfig, writeCloudConfig,
|
|
16
|
+
import { readCloudConfig, writeCloudConfig, getToken, sync as cloudSync, configPath } from "../sync/client.js";
|
|
17
17
|
import { resolveKnowledgeConfig } from "../config.js";
|
|
18
18
|
import { ingestAll } from "../knowledge/ingest.js";
|
|
19
19
|
import { isAllowedSyncServerUrl } from "../security/url.js";
|
|
@@ -65,7 +65,7 @@ export function startUiServer(store, repoRoot, port = 4517) {
|
|
|
65
65
|
if (autoSyncMinutes > 0) {
|
|
66
66
|
const timer = setInterval(() => {
|
|
67
67
|
const cloud = readCloudConfig(repoRoot);
|
|
68
|
-
if (!cloud || !getToken(cloud.server))
|
|
68
|
+
if (!cloud || !getToken(cloud.server, repoRoot))
|
|
69
69
|
return;
|
|
70
70
|
cloudSync(store, repoRoot).catch(() => undefined);
|
|
71
71
|
}, autoSyncMinutes * 60 * 1000);
|
|
@@ -136,7 +136,7 @@ export function startUiServer(store, repoRoot, port = 4517) {
|
|
|
136
136
|
proposals: store.listProposals("PENDING", 200),
|
|
137
137
|
summary: store.statusSummary(),
|
|
138
138
|
cloud: cloud
|
|
139
|
-
? { server: cloud.server, brain: cloud.brain, hasToken: !!getToken(cloud.server) }
|
|
139
|
+
? { server: cloud.server, brain: cloud.brain, hasToken: !!getToken(cloud.server, repoRoot) }
|
|
140
140
|
: null,
|
|
141
141
|
tickets: tcfg.provider
|
|
142
142
|
? {
|
|
@@ -144,7 +144,7 @@ export function startUiServer(store, repoRoot, port = 4517) {
|
|
|
144
144
|
baseUrl: tcfg.baseUrl ?? null,
|
|
145
145
|
pattern: tcfg.pattern ?? DEFAULT_TICKET_PATTERN,
|
|
146
146
|
hasCredential: tcfg.provider === "remote"
|
|
147
|
-
? !!(cloud && getToken(cloud.server))
|
|
147
|
+
? !!(cloud && getToken(cloud.server, repoRoot))
|
|
148
148
|
: !!getTicketCredential(tcfg.baseUrl ?? "linear"),
|
|
149
149
|
branch: tcfg.branch ?? null,
|
|
150
150
|
}
|
|
@@ -232,10 +232,21 @@ export function startUiServer(store, repoRoot, port = 4517) {
|
|
|
232
232
|
json(res, 400, { error: "invalid server URL" });
|
|
233
233
|
return;
|
|
234
234
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
235
|
+
// Save to project config with token
|
|
236
|
+
const p = configPath(repoRoot);
|
|
237
|
+
let existing = {};
|
|
238
|
+
try {
|
|
239
|
+
existing = JSON.parse(readFileSync(p, "utf8"));
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
// fresh file
|
|
243
|
+
}
|
|
244
|
+
const newConfig = { ...existing, server: serverUrl, brain: String(b.brain) };
|
|
245
|
+
if (b.token) {
|
|
246
|
+
newConfig.token = String(b.token);
|
|
247
|
+
}
|
|
248
|
+
writeFileSync(p, JSON.stringify(newConfig, null, 2) + "\n");
|
|
249
|
+
json(res, 200, { ok: true, hasToken: !!getToken(serverUrl, repoRoot) });
|
|
239
250
|
return;
|
|
240
251
|
}
|
|
241
252
|
if (req.method === "POST" && path === "/api/cloud/unlink") {
|