localdeck 1.5.0 → 1.6.2
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/CHANGELOG.md +38 -0
- package/README.md +1 -1
- package/THIRD_PARTY_NOTICES.md +69 -0
- package/dashboard/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/dashboard/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/dashboard/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/dashboard/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/dashboard/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
- package/dashboard/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
- package/dashboard/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
- package/dashboard/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
- package/dashboard/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
- package/dashboard/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
- package/dashboard/assets/index-CcN_iA3K.js +21 -0
- package/dashboard/assets/index-DDw1Wq-H.css +1 -0
- package/dashboard/index.html +2 -2
- package/dist/{index-tv699dzb.js → index-xsyz4cc9.js} +284 -69
- package/dist/index.js +98 -13
- package/dist/{mcp-c138pjc5.js → mcp-210n1fxv.js} +31 -3
- package/dist/{update-a6z3akjx.js → update-hvkw7mkw.js} +1 -1
- package/docs/commands.md +15 -2
- package/docs/configuration.md +42 -6
- package/docs/dashboard.md +40 -20
- package/docs/mcp.md +5 -3
- package/package.json +1 -1
- package/dashboard/assets/index-CCtEO4zm.css +0 -1
- package/dashboard/assets/index-DGUiiFV-.js +0 -21
|
@@ -2977,6 +2977,7 @@ function writePrivateJson(file, value) {
|
|
|
2977
2977
|
}
|
|
2978
2978
|
|
|
2979
2979
|
// ../daemon/src/project-config.ts
|
|
2980
|
+
import { createHash } from "node:crypto";
|
|
2980
2981
|
import fs2 from "node:fs";
|
|
2981
2982
|
import path2 from "node:path";
|
|
2982
2983
|
var DEFAULT_READY_TIMEOUT_MS = 60000;
|
|
@@ -3003,7 +3004,7 @@ function loadProject(from = process.cwd()) {
|
|
|
3003
3004
|
function loadProjectFile(file) {
|
|
3004
3005
|
const raw = readProjectJson(file);
|
|
3005
3006
|
const commands = normalize(raw, file);
|
|
3006
|
-
const profiles = parseProfiles(raw, commands, file);
|
|
3007
|
+
const { profiles, legacyProfiles } = parseProfiles(raw, commands, file);
|
|
3007
3008
|
let nickname;
|
|
3008
3009
|
if (raw && typeof raw === "object" && !Array.isArray(raw) && (Array.isArray(raw.commands) || Array.isArray(raw.services))) {
|
|
3009
3010
|
const value = raw.nickname;
|
|
@@ -3013,7 +3014,7 @@ function loadProjectFile(file) {
|
|
|
3013
3014
|
nickname = value.toLowerCase();
|
|
3014
3015
|
}
|
|
3015
3016
|
}
|
|
3016
|
-
return { file, dir: path2.dirname(file), commands, ...profiles ? { profiles } : {}, ...nickname ? { nickname } : {} };
|
|
3017
|
+
return { file, dir: path2.dirname(file), commands, ...profiles ? { profiles } : {}, ...legacyProfiles ? { legacyProfiles } : {}, ...nickname ? { nickname } : {} };
|
|
3017
3018
|
}
|
|
3018
3019
|
function updateProjectNickname(file, nickname) {
|
|
3019
3020
|
const raw = readProjectJson(file);
|
|
@@ -3174,35 +3175,8 @@ function normalize(raw, file) {
|
|
|
3174
3175
|
if (serviceType && serviceType !== "http" && (entry.upstream == null || entry.port != null)) {
|
|
3175
3176
|
throw new Error(`${path2.basename(file)}: database services need an explicit upstream (published port) and must not set an HTTP proxy port`);
|
|
3176
3177
|
}
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
if (!entry.env || typeof entry.env !== "object" || Array.isArray(entry.env))
|
|
3180
|
-
throw new Error(`${path2.basename(file)}: env must be an object of strings`);
|
|
3181
|
-
env = {};
|
|
3182
|
-
for (const [key, value2] of Object.entries(entry.env)) {
|
|
3183
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || typeof value2 !== "string" || value2.includes("\x00"))
|
|
3184
|
-
throw new Error(`${path2.basename(file)}: env must contain valid variable names and string values`);
|
|
3185
|
-
if (key === "PORT" || key.startsWith("LOCALDECK_"))
|
|
3186
|
-
throw new Error(`${path2.basename(file)}: ${key} is reserved by LocalDeck`);
|
|
3187
|
-
Object.defineProperty(env, key, { value: value2, enumerable: true, writable: true, configurable: true });
|
|
3188
|
-
}
|
|
3189
|
-
}
|
|
3190
|
-
let secrets;
|
|
3191
|
-
if (entry.secrets !== undefined) {
|
|
3192
|
-
if (!Array.isArray(entry.secrets) || !entry.secrets.every((value2) => typeof value2 === "string" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(value2))) {
|
|
3193
|
-
throw new Error(`${path2.basename(file)}: secrets must contain valid environment names`);
|
|
3194
|
-
}
|
|
3195
|
-
secrets = [];
|
|
3196
|
-
for (const secret of entry.secrets) {
|
|
3197
|
-
if (secret === "PORT" || secret.startsWith("LOCALDECK_"))
|
|
3198
|
-
throw new Error(`${path2.basename(file)}: ${secret} is reserved by LocalDeck`);
|
|
3199
|
-
if (env && Object.prototype.hasOwnProperty.call(env, secret))
|
|
3200
|
-
throw new Error(`${path2.basename(file)}: ${secret} cannot be both a secret and env setting`);
|
|
3201
|
-
if (secrets.includes(secret))
|
|
3202
|
-
throw new Error(`${path2.basename(file)}: duplicate secret ${secret}`);
|
|
3203
|
-
secrets.push(secret);
|
|
3204
|
-
}
|
|
3205
|
-
}
|
|
3178
|
+
const env = entry.env === undefined ? undefined : parseEnv(entry.env, file);
|
|
3179
|
+
const secrets = entry.secrets === undefined ? undefined : parseSecrets(entry.secrets, env, file);
|
|
3206
3180
|
out.push({
|
|
3207
3181
|
name,
|
|
3208
3182
|
run,
|
|
@@ -3220,6 +3194,35 @@ function normalize(raw, file) {
|
|
|
3220
3194
|
validateDependencies(out, file);
|
|
3221
3195
|
return out;
|
|
3222
3196
|
}
|
|
3197
|
+
function parseEnv(value, file) {
|
|
3198
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
3199
|
+
throw new Error(`${path2.basename(file)}: env must be an object of strings`);
|
|
3200
|
+
const env = {};
|
|
3201
|
+
for (const [key, setting] of Object.entries(value)) {
|
|
3202
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || typeof setting !== "string" || setting.includes("\x00"))
|
|
3203
|
+
throw new Error(`${path2.basename(file)}: env must contain valid variable names and string values`);
|
|
3204
|
+
if (key === "PORT" || key.startsWith("LOCALDECK_"))
|
|
3205
|
+
throw new Error(`${path2.basename(file)}: ${key} is reserved by LocalDeck`);
|
|
3206
|
+
Object.defineProperty(env, key, { value: setting, enumerable: true, writable: true, configurable: true });
|
|
3207
|
+
}
|
|
3208
|
+
return env;
|
|
3209
|
+
}
|
|
3210
|
+
function parseSecrets(value, env, file) {
|
|
3211
|
+
if (!Array.isArray(value) || !value.every((name) => typeof name === "string" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(name))) {
|
|
3212
|
+
throw new Error(`${path2.basename(file)}: secrets must contain valid environment names`);
|
|
3213
|
+
}
|
|
3214
|
+
const secrets = [];
|
|
3215
|
+
for (const secret of value) {
|
|
3216
|
+
if (secret === "PORT" || secret.startsWith("LOCALDECK_"))
|
|
3217
|
+
throw new Error(`${path2.basename(file)}: ${secret} is reserved by LocalDeck`);
|
|
3218
|
+
if (env && Object.prototype.hasOwnProperty.call(env, secret))
|
|
3219
|
+
throw new Error(`${path2.basename(file)}: ${secret} cannot be both a secret and env setting`);
|
|
3220
|
+
if (secrets.includes(secret))
|
|
3221
|
+
throw new Error(`${path2.basename(file)}: duplicate secret ${secret}`);
|
|
3222
|
+
secrets.push(secret);
|
|
3223
|
+
}
|
|
3224
|
+
return secrets;
|
|
3225
|
+
}
|
|
3223
3226
|
function parseReadiness(value, file, index) {
|
|
3224
3227
|
if (value === "postgres" || value === "redis") {
|
|
3225
3228
|
return { type: "command", run: value === "postgres" ? "pg_isready -h 127.0.0.1 -p {upstream}" : "redis-cli -h 127.0.0.1 -p {upstream} ping", ...value === "redis" ? { expectedOutput: "PONG" } : {}, timeoutMs: DEFAULT_READY_TIMEOUT_MS, intervalMs: DEFAULT_READY_INTERVAL_MS };
|
|
@@ -3264,22 +3267,159 @@ function parseReadiness(value, file, index) {
|
|
|
3264
3267
|
throw new Error(`${path2.basename(file)}: expectedOutput must be a string of at most 4000 characters`);
|
|
3265
3268
|
return { type, run: ready.run, ...ready.expectedOutput === undefined ? {} : { expectedOutput: ready.expectedOutput }, timeoutMs, intervalMs };
|
|
3266
3269
|
}
|
|
3270
|
+
var OVERRIDE_FIELDS = ["run", "cwd", "env", "secrets"];
|
|
3267
3271
|
function parseProfiles(raw, commands, file) {
|
|
3268
3272
|
if (!raw || typeof raw !== "object" || Array.isArray(raw) || !Array.isArray(raw.services) && !Array.isArray(raw.commands))
|
|
3269
|
-
return;
|
|
3273
|
+
return {};
|
|
3270
3274
|
const value = raw.profiles;
|
|
3271
3275
|
if (value === undefined)
|
|
3272
|
-
return;
|
|
3276
|
+
return {};
|
|
3277
|
+
const base = path2.basename(file);
|
|
3273
3278
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
3274
|
-
throw new Error(`${
|
|
3275
|
-
const
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
+
throw new Error(`${base}: profiles must map names to profile objects`);
|
|
3280
|
+
const entries = Object.entries(value);
|
|
3281
|
+
if (!entries.length)
|
|
3282
|
+
return {};
|
|
3283
|
+
for (const [name] of entries)
|
|
3284
|
+
if (!/^[a-z0-9][a-z0-9_-]{0,40}$/.test(name))
|
|
3285
|
+
throw new Error(`${base}: invalid profile name "${name}"`);
|
|
3286
|
+
const lists = entries.filter(([, profile]) => Array.isArray(profile));
|
|
3287
|
+
if (lists.length && lists.length !== entries.length)
|
|
3288
|
+
throw new Error(`${base}: profiles cannot mix service lists and profile objects; move service lists to tags`);
|
|
3289
|
+
const byName = new Map(commands.map((command) => [command.name, command]));
|
|
3290
|
+
if (lists.length) {
|
|
3291
|
+
const legacyProfiles = Object.create(null);
|
|
3292
|
+
for (const [name, services] of lists) {
|
|
3293
|
+
if (!services.length || !services.every((service) => typeof service === "string" && byName.has(service.toLowerCase()))) {
|
|
3294
|
+
throw new Error(`${base}: profile ${name} must contain existing service names`);
|
|
3295
|
+
}
|
|
3296
|
+
legacyProfiles[name] = [...new Set(services.map((service) => service.toLowerCase()))];
|
|
3297
|
+
for (const service of legacyProfiles[name]) {
|
|
3298
|
+
const command = byName.get(service);
|
|
3299
|
+
if (!command.tags.includes(name))
|
|
3300
|
+
command.tags.push(name);
|
|
3301
|
+
}
|
|
3302
|
+
}
|
|
3303
|
+
return { legacyProfiles };
|
|
3304
|
+
}
|
|
3305
|
+
const profiles = [];
|
|
3306
|
+
for (const [name, entry] of entries) {
|
|
3307
|
+
if (!entry || typeof entry !== "object")
|
|
3308
|
+
throw new Error(`${base}: profile ${name} must be an object`);
|
|
3309
|
+
const record = entry;
|
|
3310
|
+
const unknownKey = Object.keys(record).find((key) => key !== "description" && key !== "services");
|
|
3311
|
+
if (unknownKey)
|
|
3312
|
+
throw new Error(`${base}: profile ${name} has unknown field "${unknownKey}"; use "description" and "services"`);
|
|
3313
|
+
if (record.description !== undefined && (typeof record.description !== "string" || record.description.length > 200))
|
|
3314
|
+
throw new Error(`${base}: profile ${name} description must be a string of at most 200 characters`);
|
|
3315
|
+
const services = record.services ?? {};
|
|
3316
|
+
if (!services || typeof services !== "object" || Array.isArray(services))
|
|
3317
|
+
throw new Error(`${base}: profile ${name} services must map service names to overrides`);
|
|
3318
|
+
const overrides = Object.create(null);
|
|
3319
|
+
for (const [rawService, rawOverride] of Object.entries(services)) {
|
|
3320
|
+
const service = rawService.toLowerCase();
|
|
3321
|
+
const command = byName.get(service);
|
|
3322
|
+
if (!command)
|
|
3323
|
+
throw new Error(`${base}: profile ${name} changes unknown service "${rawService}"`);
|
|
3324
|
+
if (!rawOverride || typeof rawOverride !== "object" || Array.isArray(rawOverride))
|
|
3325
|
+
throw new Error(`${base}: profile ${name} override for ${service} must be an object`);
|
|
3326
|
+
const override = rawOverride;
|
|
3327
|
+
const field = Object.keys(override).find((key) => !OVERRIDE_FIELDS.includes(key));
|
|
3328
|
+
if (field)
|
|
3329
|
+
throw new Error(`${base}: profile ${name} cannot change ${service}.${field}; profiles can change run, cwd, env and secrets`);
|
|
3330
|
+
if (override.run !== undefined && (typeof override.run !== "string" || !override.run.trim()))
|
|
3331
|
+
throw new Error(`${base}: profile ${name} ${service}.run must be a non-empty string`);
|
|
3332
|
+
if (override.cwd !== undefined && typeof override.cwd !== "string")
|
|
3333
|
+
throw new Error(`${base}: profile ${name} ${service}.cwd must be a string`);
|
|
3334
|
+
const env = override.env === undefined ? undefined : parseEnv(override.env, file);
|
|
3335
|
+
const secrets = override.secrets === undefined ? undefined : parseSecrets(override.secrets, env, file);
|
|
3336
|
+
const parsed = {
|
|
3337
|
+
...override.run === undefined ? {} : { run: override.run },
|
|
3338
|
+
...override.cwd === undefined ? {} : { cwd: override.cwd },
|
|
3339
|
+
...env ? { env } : {},
|
|
3340
|
+
...secrets ? { secrets } : {}
|
|
3341
|
+
};
|
|
3342
|
+
checkEnvSecretOverlap(mergeOverride(command, parsed).command, `profile ${name}`, file);
|
|
3343
|
+
overrides[service] = parsed;
|
|
3279
3344
|
}
|
|
3280
|
-
profiles
|
|
3345
|
+
profiles.push({ name, ...record.description ? { description: record.description } : {}, services: overrides });
|
|
3281
3346
|
}
|
|
3282
|
-
return profiles;
|
|
3347
|
+
return { profiles };
|
|
3348
|
+
}
|
|
3349
|
+
function mergeOverride(command, override) {
|
|
3350
|
+
const env = override.env ? { ...command.env, ...override.env } : command.env;
|
|
3351
|
+
const secrets = override.secrets ? [...new Set([...command.secrets ?? [], ...override.secrets])] : command.secrets;
|
|
3352
|
+
return {
|
|
3353
|
+
command: {
|
|
3354
|
+
...command,
|
|
3355
|
+
...override.run === undefined ? {} : { run: override.run },
|
|
3356
|
+
...override.cwd === undefined ? {} : { cwd: override.cwd },
|
|
3357
|
+
...env ? { env } : {},
|
|
3358
|
+
...secrets ? { secrets } : {}
|
|
3359
|
+
}
|
|
3360
|
+
};
|
|
3361
|
+
}
|
|
3362
|
+
function checkEnvSecretOverlap(command, source, file) {
|
|
3363
|
+
const overlap = command.secrets?.find((secret) => command.env && Object.prototype.hasOwnProperty.call(command.env, secret));
|
|
3364
|
+
if (overlap)
|
|
3365
|
+
throw new Error(`${path2.basename(file)}: ${source} makes ${overlap} both a secret and env setting for ${command.name}`);
|
|
3366
|
+
}
|
|
3367
|
+
function applyProfiles(project, active = []) {
|
|
3368
|
+
const wanted = new Set(active);
|
|
3369
|
+
const applied = (project.profiles ?? []).filter((profile) => wanted.has(profile.name));
|
|
3370
|
+
const provenance = Object.create(null);
|
|
3371
|
+
const setters = new Map;
|
|
3372
|
+
const commands = project.commands.map((base) => {
|
|
3373
|
+
const origin = {
|
|
3374
|
+
run: "base",
|
|
3375
|
+
cwd: "base",
|
|
3376
|
+
env: Object.fromEntries(Object.keys(base.env ?? {}).map((key) => [key, "base"])),
|
|
3377
|
+
secrets: Object.fromEntries((base.secrets ?? []).map((secret) => [secret, "base"]))
|
|
3378
|
+
};
|
|
3379
|
+
let command = base;
|
|
3380
|
+
for (const profile of applied) {
|
|
3381
|
+
const override = profile.services[base.name];
|
|
3382
|
+
if (!override)
|
|
3383
|
+
continue;
|
|
3384
|
+
const note = (field) => {
|
|
3385
|
+
const key = `${base.name}\x00${field}`;
|
|
3386
|
+
setters.set(key, [...setters.get(key) ?? [], profile.name]);
|
|
3387
|
+
};
|
|
3388
|
+
if (override.run !== undefined) {
|
|
3389
|
+
origin.run = profile.name;
|
|
3390
|
+
note("run");
|
|
3391
|
+
}
|
|
3392
|
+
if (override.cwd !== undefined) {
|
|
3393
|
+
origin.cwd = profile.name;
|
|
3394
|
+
note("cwd");
|
|
3395
|
+
}
|
|
3396
|
+
for (const key of Object.keys(override.env ?? {})) {
|
|
3397
|
+
origin.env[key] = profile.name;
|
|
3398
|
+
note(`env.${key}`);
|
|
3399
|
+
}
|
|
3400
|
+
for (const secret of override.secrets ?? [])
|
|
3401
|
+
if (!(secret in origin.secrets))
|
|
3402
|
+
origin.secrets[secret] = profile.name;
|
|
3403
|
+
command = mergeOverride(command, override).command;
|
|
3404
|
+
}
|
|
3405
|
+
if (command !== base)
|
|
3406
|
+
checkEnvSecretOverlap(command, `active profiles ${applied.map((profile) => profile.name).join(" + ")}`, project.file);
|
|
3407
|
+
provenance[base.name] = origin;
|
|
3408
|
+
return command;
|
|
3409
|
+
});
|
|
3410
|
+
const clashes = [];
|
|
3411
|
+
for (const [key, profiles] of setters) {
|
|
3412
|
+
if (profiles.length < 2)
|
|
3413
|
+
continue;
|
|
3414
|
+
const [service, field] = key.split("\x00");
|
|
3415
|
+
clashes.push({ service, field, profiles, winner: profiles[profiles.length - 1] });
|
|
3416
|
+
}
|
|
3417
|
+
return { commands, active: applied.map((profile) => profile.name), provenance, clashes };
|
|
3418
|
+
}
|
|
3419
|
+
function commandFingerprint(command) {
|
|
3420
|
+
const env = Object.entries(command.env ?? {}).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
3421
|
+
const secrets = [...command.secrets ?? []].sort();
|
|
3422
|
+
return createHash("sha256").update(JSON.stringify([command.run, command.cwd ?? "", env, secrets])).digest("hex").slice(0, 16);
|
|
3283
3423
|
}
|
|
3284
3424
|
function parseBoundedInteger(value, fallback, min, max, label, file, index) {
|
|
3285
3425
|
if (value === undefined)
|
|
@@ -3424,10 +3564,10 @@ import path8 from "node:path";
|
|
|
3424
3564
|
// ../daemon/src/worktrees.ts
|
|
3425
3565
|
import fs4 from "node:fs";
|
|
3426
3566
|
import path4 from "node:path";
|
|
3427
|
-
import { createHash as
|
|
3567
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
3428
3568
|
|
|
3429
3569
|
// ../daemon/src/projects.ts
|
|
3430
|
-
import { createHash } from "node:crypto";
|
|
3570
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
3431
3571
|
import fs3 from "node:fs";
|
|
3432
3572
|
import { execFileSync } from "node:child_process";
|
|
3433
3573
|
import path3 from "node:path";
|
|
@@ -3559,19 +3699,40 @@ class ProjectRegistry {
|
|
|
3559
3699
|
list() {
|
|
3560
3700
|
return this.sorted().map((project) => this.toSummary(project));
|
|
3561
3701
|
}
|
|
3562
|
-
details(projectId) {
|
|
3702
|
+
details(projectId, profiles) {
|
|
3563
3703
|
const project = this.require(projectId);
|
|
3704
|
+
let loaded;
|
|
3564
3705
|
try {
|
|
3565
|
-
|
|
3566
|
-
return { ...project, nickname: this.nickname(projectId), available: true, commands: loaded.commands, ...loaded.profiles ? { profiles: loaded.profiles } : {} };
|
|
3706
|
+
loaded = this.load(project);
|
|
3567
3707
|
} catch (error) {
|
|
3568
3708
|
return { ...project, available: false, error: errorMessage(error), commands: [] };
|
|
3569
3709
|
}
|
|
3710
|
+
const wanted = profiles ?? project.activeProfiles ?? [];
|
|
3711
|
+
const { activeProfiles: _stored, ...remembered } = project;
|
|
3712
|
+
const base = { ...remembered, nickname: this.nickname(projectId), available: true, ...loaded.legacyProfiles ? { legacyProfiles: loaded.legacyProfiles } : {} };
|
|
3713
|
+
const summaries = (active) => loaded.profiles?.length ? { profiles: loaded.profiles.map((profile) => ({ name: profile.name, ...profile.description ? { description: profile.description } : {}, services: Object.keys(profile.services), active: active.includes(profile.name) })) } : {};
|
|
3714
|
+
try {
|
|
3715
|
+
const resolved = applyProfiles(loaded, wanted);
|
|
3716
|
+
return {
|
|
3717
|
+
...base,
|
|
3718
|
+
...resolved.active.length ? { activeProfiles: resolved.active } : {},
|
|
3719
|
+
commands: resolved.commands,
|
|
3720
|
+
...summaries(resolved.active),
|
|
3721
|
+
...resolved.clashes.length ? { profileClashes: resolved.clashes } : {},
|
|
3722
|
+
provenance: resolved.provenance,
|
|
3723
|
+
fingerprints: Object.fromEntries(resolved.commands.map((command) => [command.name, commandFingerprint(command)]))
|
|
3724
|
+
};
|
|
3725
|
+
} catch (error) {
|
|
3726
|
+
const active = (loaded.profiles ?? []).map((profile) => profile.name).filter((name) => wanted.includes(name));
|
|
3727
|
+
return { ...base, ...active.length ? { activeProfiles: active } : {}, commands: loaded.commands, ...summaries(active), profileError: errorMessage(error) };
|
|
3728
|
+
}
|
|
3570
3729
|
}
|
|
3571
|
-
resolve(projectId, serviceName2) {
|
|
3572
|
-
const details = this.details(projectId);
|
|
3730
|
+
resolve(projectId, serviceName2, profiles) {
|
|
3731
|
+
const details = this.details(projectId, profiles);
|
|
3573
3732
|
if (!details.available)
|
|
3574
3733
|
throw new ProjectError("invalid", details.error);
|
|
3734
|
+
if (details.profileError)
|
|
3735
|
+
throw new ProjectError("invalid", details.profileError);
|
|
3575
3736
|
const command = details.commands.find((candidate) => candidate.name === serviceName2.toLowerCase());
|
|
3576
3737
|
if (!command)
|
|
3577
3738
|
throw new ProjectError("not_found", `Project ${projectId} has no service named ${serviceName2}`);
|
|
@@ -3632,6 +3793,37 @@ class ProjectRegistry {
|
|
|
3632
3793
|
project.configNickname = nickname || undefined;
|
|
3633
3794
|
return this.updateNickname(projectId, "");
|
|
3634
3795
|
}
|
|
3796
|
+
setActiveProfiles(projectId, names) {
|
|
3797
|
+
const project = this.require(projectId);
|
|
3798
|
+
if (!Array.isArray(names) || !names.every((name) => typeof name === "string"))
|
|
3799
|
+
throw new ProjectError("invalid", "profiles must be an array of profile names");
|
|
3800
|
+
let defined;
|
|
3801
|
+
try {
|
|
3802
|
+
defined = (this.load(project).profiles ?? []).map((profile) => profile.name);
|
|
3803
|
+
} catch (error) {
|
|
3804
|
+
throw new ProjectError("invalid", errorMessage(error));
|
|
3805
|
+
}
|
|
3806
|
+
const requested = names.map((name) => name.toLowerCase());
|
|
3807
|
+
const unknown = requested.filter((name) => !defined.includes(name));
|
|
3808
|
+
if (unknown.length)
|
|
3809
|
+
throw new ProjectError("invalid", `Unknown profile${unknown.length > 1 ? "s" : ""} ${unknown.join(", ")}. Available profiles: ${defined.join(", ") || "(none)"}`);
|
|
3810
|
+
const previous = project.activeProfiles;
|
|
3811
|
+
const active = defined.filter((name) => requested.includes(name));
|
|
3812
|
+
if (active.length)
|
|
3813
|
+
project.activeProfiles = active;
|
|
3814
|
+
else
|
|
3815
|
+
delete project.activeProfiles;
|
|
3816
|
+
try {
|
|
3817
|
+
this.persist();
|
|
3818
|
+
} catch (error) {
|
|
3819
|
+
if (previous === undefined)
|
|
3820
|
+
delete project.activeProfiles;
|
|
3821
|
+
else
|
|
3822
|
+
project.activeProfiles = previous;
|
|
3823
|
+
throw error;
|
|
3824
|
+
}
|
|
3825
|
+
return this.details(projectId);
|
|
3826
|
+
}
|
|
3635
3827
|
updateDependencies(projectId, serviceName2, after) {
|
|
3636
3828
|
const project = this.require(projectId);
|
|
3637
3829
|
try {
|
|
@@ -3750,7 +3942,7 @@ class ProjectRegistry {
|
|
|
3750
3942
|
}
|
|
3751
3943
|
}
|
|
3752
3944
|
function projectId(realFile) {
|
|
3753
|
-
return
|
|
3945
|
+
return createHash2("sha256").update(realFile).digest("hex").slice(0, 16);
|
|
3754
3946
|
}
|
|
3755
3947
|
function slugify(value) {
|
|
3756
3948
|
return value.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -3768,7 +3960,7 @@ function isRememberedProject(value) {
|
|
|
3768
3960
|
if (!["id", "name", "slug", "file", "directory"].every((key) => typeof project[key] === "string"))
|
|
3769
3961
|
return false;
|
|
3770
3962
|
const { id, slug, file, directory } = project;
|
|
3771
|
-
return path3.isAbsolute(file) && path3.normalize(file) === file && directory === path3.dirname(file) && /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(slug) && /^[a-f0-9]{16}$/.test(id) && (project.nickname === undefined || typeof project.nickname === "string" && validNickname(project.nickname)) && (project.configNickname === undefined || typeof project.configNickname === "string" && validNickname(project.configNickname)) && (project.sharedConfigNickname === undefined || typeof project.sharedConfigNickname === "boolean") && id === projectId(file) && typeof project.lastUsedAt === "number" && Number.isFinite(project.lastUsedAt);
|
|
3963
|
+
return path3.isAbsolute(file) && path3.normalize(file) === file && directory === path3.dirname(file) && /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(slug) && /^[a-f0-9]{16}$/.test(id) && (project.nickname === undefined || typeof project.nickname === "string" && validNickname(project.nickname)) && (project.configNickname === undefined || typeof project.configNickname === "string" && validNickname(project.configNickname)) && (project.sharedConfigNickname === undefined || typeof project.sharedConfigNickname === "boolean") && (project.activeProfiles === undefined || Array.isArray(project.activeProfiles) && project.activeProfiles.every((name) => typeof name === "string" && /^[a-z0-9][a-z0-9_-]{0,40}$/.test(name))) && id === projectId(file) && typeof project.lastUsedAt === "number" && Number.isFinite(project.lastUsedAt);
|
|
3772
3964
|
}
|
|
3773
3965
|
function validNickname(value) {
|
|
3774
3966
|
return value.length <= 63 && /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(value);
|
|
@@ -3834,7 +4026,7 @@ function discoverWorktrees(projects, projectId2) {
|
|
|
3834
4026
|
const known = registered.get(configFile);
|
|
3835
4027
|
const branch = fields.find((field) => field.startsWith("branch "))?.slice(7).replace(/^refs\/heads\//, "");
|
|
3836
4028
|
environments.push({
|
|
3837
|
-
id:
|
|
4029
|
+
id: createHash3("sha256").update(configFile).digest("hex").slice(0, 16),
|
|
3838
4030
|
...known ? { projectId: known.id } : {},
|
|
3839
4031
|
directory,
|
|
3840
4032
|
configFile,
|
|
@@ -6134,16 +6326,20 @@ async function handleApi(req, res, url, parts, deps) {
|
|
|
6134
6326
|
const secretValues = Object.values((deps.resolveProjectSecrets ?? resolveProjectSecrets)(projectId2, names));
|
|
6135
6327
|
return json(res, 200, { report: diagnosticReport(project, services, logs, deps.version, [deps.controlToken, ...secretValues]) });
|
|
6136
6328
|
}
|
|
6137
|
-
if (parts.length ===
|
|
6329
|
+
if (parts.length === 3 && parts[2] === "profiles" && method === "GET") {
|
|
6330
|
+
const details = requireRememberedProject(projects, projectId2);
|
|
6331
|
+
if (!details.available)
|
|
6332
|
+
throw new ProjectError("invalid", details.error ?? "project configuration is invalid");
|
|
6333
|
+
return json(res, 200, profilesResponse(details));
|
|
6334
|
+
}
|
|
6335
|
+
if (parts.length === 4 && parts[2] === "profiles" && parts[3] === "active" && method === "PUT") {
|
|
6138
6336
|
requireMutationAllowed(deps);
|
|
6139
|
-
await readBody(req);
|
|
6337
|
+
const body2 = await readBody(req);
|
|
6140
6338
|
requireMutationAllowed(deps);
|
|
6141
|
-
|
|
6142
|
-
|
|
6143
|
-
|
|
6144
|
-
|
|
6145
|
-
const results = await Promise.all(names.map((name) => supervisor.start(projectId2, name)));
|
|
6146
|
-
return json(res, 200, { ok: results.every((result) => result.ok), results });
|
|
6339
|
+
if (!body2 || typeof body2 !== "object" || Array.isArray(body2) || Object.keys(body2).some((key) => key !== "profiles"))
|
|
6340
|
+
throw new HttpError(400, 'request body must be { "profiles": [...] }');
|
|
6341
|
+
const details = projects.setActiveProfiles(projectId2, body2.profiles);
|
|
6342
|
+
return json(res, 200, profilesResponse(details));
|
|
6147
6343
|
}
|
|
6148
6344
|
if (parts.length === 2 && method === "GET") {
|
|
6149
6345
|
const details = projects.details(projectId2);
|
|
@@ -6459,6 +6655,8 @@ function parseAttachMessage(raw) {
|
|
|
6459
6655
|
if (input.upstreamSource !== undefined && input.upstreamSource !== "assigned" && input.upstreamSource !== "manual")
|
|
6460
6656
|
throw new Error("upstreamSource must be assigned or manual");
|
|
6461
6657
|
optionalPid(input.pid);
|
|
6658
|
+
if (input.profiles !== undefined && (!Array.isArray(input.profiles) || input.profiles.length > 64 || !input.profiles.every((name) => typeof name === "string" && /^[a-z0-9][a-z0-9_-]{0,40}$/.test(name))))
|
|
6659
|
+
throw new Error("profiles must be an array of profile names");
|
|
6462
6660
|
return input;
|
|
6463
6661
|
}
|
|
6464
6662
|
if (input.type === "log") {
|
|
@@ -6541,10 +6739,10 @@ function buildRegistration(message, deps) {
|
|
|
6541
6739
|
pid: message.pid
|
|
6542
6740
|
};
|
|
6543
6741
|
}
|
|
6544
|
-
const project = deps.projects.details(requestedProject);
|
|
6742
|
+
const project = deps.projects.details(requestedProject, message.profiles);
|
|
6545
6743
|
if (!project.available)
|
|
6546
6744
|
throw new ProjectError("invalid", project.error ?? "project configuration is invalid");
|
|
6547
|
-
const command = deps.projects.resolve(requestedProject, canonicalName);
|
|
6745
|
+
const command = deps.projects.resolve(requestedProject, canonicalName, message.profiles);
|
|
6548
6746
|
if (command.serviceType && command.serviceType !== "http" && (message.upstreamPort !== command.upstream || message.stablePort !== undefined)) {
|
|
6549
6747
|
throw new Error("database registration must use its configured published upstream port");
|
|
6550
6748
|
}
|
|
@@ -6563,7 +6761,18 @@ function buildRegistration(message, deps) {
|
|
|
6563
6761
|
upstreamSource: message.upstreamSource ?? (command.upstream === undefined ? "assigned" : "manual"),
|
|
6564
6762
|
cwd,
|
|
6565
6763
|
command: command.run,
|
|
6566
|
-
pid: message.pid
|
|
6764
|
+
pid: message.pid,
|
|
6765
|
+
...project.activeProfiles?.length ? { profiles: project.activeProfiles } : {},
|
|
6766
|
+
configFingerprint: commandFingerprint(command)
|
|
6767
|
+
};
|
|
6768
|
+
}
|
|
6769
|
+
function profilesResponse(details) {
|
|
6770
|
+
return {
|
|
6771
|
+
profiles: details.profiles ?? [],
|
|
6772
|
+
active: details.activeProfiles ?? [],
|
|
6773
|
+
clashes: details.profileClashes ?? [],
|
|
6774
|
+
...details.profileError ? { error: details.profileError } : {},
|
|
6775
|
+
...details.legacyProfiles ? { legacyProfiles: details.legacyProfiles } : {}
|
|
6567
6776
|
};
|
|
6568
6777
|
}
|
|
6569
6778
|
function requireDirectory(cwd) {
|
|
@@ -6803,6 +7012,8 @@ class Registry {
|
|
|
6803
7012
|
pid: opts.pid,
|
|
6804
7013
|
cwd: opts.cwd,
|
|
6805
7014
|
command: opts.command,
|
|
7015
|
+
...opts.profiles?.length ? { profiles: [...opts.profiles] } : {},
|
|
7016
|
+
...opts.configFingerprint ? { configFingerprint: opts.configFingerprint } : {},
|
|
6806
7017
|
startedAt: Date.now(),
|
|
6807
7018
|
upstreamReady: false,
|
|
6808
7019
|
urls: [
|
|
@@ -7042,7 +7253,7 @@ import fs11 from "node:fs";
|
|
|
7042
7253
|
// ../daemon/src/activity.ts
|
|
7043
7254
|
import fs10 from "node:fs";
|
|
7044
7255
|
import path11 from "node:path";
|
|
7045
|
-
import { randomUUID as randomUUID3, createHash as
|
|
7256
|
+
import { randomUUID as randomUUID3, createHash as createHash4 } from "node:crypto";
|
|
7046
7257
|
|
|
7047
7258
|
// ../daemon/src/history.ts
|
|
7048
7259
|
function isAfterHistoryBoundary(id, boundary) {
|
|
@@ -7077,7 +7288,7 @@ function sanitizeSignature(value, secrets2) {
|
|
|
7077
7288
|
return redactDiagnostic(querySafe, secrets2.flatMap((secret) => [secret, encodeURIComponent(secret)]));
|
|
7078
7289
|
}
|
|
7079
7290
|
function boundedSignature(value) {
|
|
7080
|
-
return value.length <= 300 ? value : `${value.slice(0, 230)}… [${
|
|
7291
|
+
return value.length <= 300 ? value : `${value.slice(0, 230)}… [${createHash4("sha256").update(value).digest("hex")}]`;
|
|
7081
7292
|
}
|
|
7082
7293
|
function validEvent(value) {
|
|
7083
7294
|
if (!value || typeof value !== "object")
|
|
@@ -7227,7 +7438,7 @@ class Activity {
|
|
|
7227
7438
|
throw new Error("Search must be at most 200 characters");
|
|
7228
7439
|
const all = this.records(projectId2, services), coverage = this.coverage(services, all);
|
|
7229
7440
|
let cursor;
|
|
7230
|
-
const scope =
|
|
7441
|
+
const scope = createHash4("sha256").update([...new Set(services)].sort().join(",")).digest("hex");
|
|
7231
7442
|
if (options.cursor) {
|
|
7232
7443
|
try {
|
|
7233
7444
|
if (options.cursor.length > 4096)
|
|
@@ -7345,7 +7556,7 @@ class ResponseBodies {
|
|
|
7345
7556
|
|
|
7346
7557
|
// ../daemon/src/store.ts
|
|
7347
7558
|
import path12 from "node:path";
|
|
7348
|
-
import { createHash as
|
|
7559
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
7349
7560
|
import { EventEmitter } from "node:events";
|
|
7350
7561
|
import { finished } from "node:stream/promises";
|
|
7351
7562
|
import { Writable } from "node:stream";
|
|
@@ -7436,7 +7647,7 @@ class Store extends EventEmitter {
|
|
|
7436
7647
|
return `${this.daemonStartedAt.toString(36)}-${(++this.sequence).toString(36)}`;
|
|
7437
7648
|
}
|
|
7438
7649
|
compatibilityId(type, service, offset, serialized) {
|
|
7439
|
-
const hash =
|
|
7650
|
+
const hash = createHash5("sha256").update(`${type}\x00${service}\x00${offset}\x00${serialized}`).digest("hex");
|
|
7440
7651
|
return `legacy-${hash}`;
|
|
7441
7652
|
}
|
|
7442
7653
|
historyFile(service, type) {
|
|
@@ -8551,7 +8762,9 @@ class ProcessSupervisor {
|
|
|
8551
8762
|
upstreamSource: command.upstream === undefined ? "assigned" : "manual",
|
|
8552
8763
|
cwd,
|
|
8553
8764
|
command: command.run,
|
|
8554
|
-
serviceType: command.serviceType
|
|
8765
|
+
serviceType: command.serviceType,
|
|
8766
|
+
...project.activeProfiles?.length ? { profiles: project.activeProfiles } : {},
|
|
8767
|
+
configFingerprint: commandFingerprint(command)
|
|
8555
8768
|
}, () => void this.stop(key, "stop requested"));
|
|
8556
8769
|
} catch (error) {
|
|
8557
8770
|
this.releaseAutomaticUpstream(reservedUpstreamPort);
|
|
@@ -9028,7 +9241,7 @@ async function waitForProcessGroupExit(groupId, timeoutMs) {
|
|
|
9028
9241
|
return true;
|
|
9029
9242
|
}
|
|
9030
9243
|
// ../daemon/package.json
|
|
9031
|
-
var version = "1.
|
|
9244
|
+
var version = "1.6.2";
|
|
9032
9245
|
|
|
9033
9246
|
// ../daemon/src/index.ts
|
|
9034
9247
|
var VERSION = version;
|
|
@@ -9308,6 +9521,8 @@ class Api {
|
|
|
9308
9521
|
shareInfo = (key) => this.call("GET", `/api/services/${encodeURIComponent(key)}/share`);
|
|
9309
9522
|
replay = (key, requestId) => this.call("POST", `/api/services/${encodeURIComponent(key)}/replay`, { requestId });
|
|
9310
9523
|
services = () => this.call("GET", "/api/services");
|
|
9524
|
+
profiles = (projectId2) => this.call("GET", `/api/projects/${encodeURIComponent(projectId2)}/profiles`);
|
|
9525
|
+
setActiveProfiles = (projectId2, profiles) => this.call("PUT", `/api/projects/${encodeURIComponent(projectId2)}/profiles/active`, { profiles });
|
|
9311
9526
|
rememberProject = (file, token) => this.call("POST", "/api/projects/remember", { file }, { authorization: `Bearer ${token}` });
|
|
9312
9527
|
async servicePath(reference) {
|
|
9313
9528
|
const separator = reference.lastIndexOf("/");
|
|
@@ -9332,4 +9547,4 @@ class Api {
|
|
|
9332
9547
|
shutdown = () => this.call("POST", "/api/shutdown");
|
|
9333
9548
|
}
|
|
9334
9549
|
|
|
9335
|
-
export { __toESM, __commonJS, __export, __require, wrapper_default, HOME_DIR, STATE_FILE, DEFAULT_API_PORT, assertUserRuntime, isPortFree, getFreePort, findProjectFile, loadProject, loadProjectFile, shellCommand, redactDiagnostic, environmentValues, listProjectSecrets, resolveProjectSecrets, startForwarder, serviceKey, processTree, listeningPorts, chooseUpstream, unreachableListenerMessage, connectHost, streamLines, killProcessTree, stripAnsi, startupGuidance, inferPortEnvironment, startupLayers, waitForReadiness, secretOutput, VERSION, startDaemon, findDaemon, dashboardDir, ensureDaemon, Api };
|
|
9550
|
+
export { __toESM, __commonJS, __export, __require, wrapper_default, HOME_DIR, STATE_FILE, DEFAULT_API_PORT, assertUserRuntime, isPortFree, getFreePort, findProjectFile, loadProject, loadProjectFile, applyProfiles, shellCommand, redactDiagnostic, environmentValues, listProjectSecrets, resolveProjectSecrets, startForwarder, serviceKey, processTree, listeningPorts, chooseUpstream, unreachableListenerMessage, connectHost, streamLines, killProcessTree, stripAnsi, startupGuidance, inferPortEnvironment, startupLayers, waitForReadiness, secretOutput, VERSION, startDaemon, findDaemon, dashboardDir, ensureDaemon, Api };
|