@netnodeag/kraftwerk 0.37.0 → 0.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/cli/projects.js +2 -1
  2. package/dist/cli/ui.js +2 -2
  3. package/dist/config.d.ts +11 -0
  4. package/dist/config.js +20 -1
  5. package/dist/inspector/instances.d.ts +8 -0
  6. package/dist/inspector/instances.js +30 -10
  7. package/dist/inspector/server.js +2 -0
  8. package/dist/inspector/settings.d.ts +2 -0
  9. package/dist/inspector/settings.js +8 -0
  10. package/inspector/dist/assets/{editor-vM6JSj5h.js → editor-BnDJd3j4.js} +3 -3
  11. package/inspector/dist/assets/{index-MWv5H1oP.js → index-1K-YGe-R.js} +1 -1
  12. package/inspector/dist/assets/{index-C1UXlW32.js → index-5n-x53jb.js} +1 -1
  13. package/inspector/dist/assets/{index-BAAjszPI.js → index-B6zeZ-I9.js} +1 -1
  14. package/inspector/dist/assets/{index-CtMGH62C.js → index-BCwvYdEE.js} +1 -1
  15. package/inspector/dist/assets/{index-DfZT0IoM.js → index-BDHHRQvJ.js} +1 -1
  16. package/inspector/dist/assets/{index-D_11IZBV.js → index-BFmXikOX.js} +1 -1
  17. package/inspector/dist/assets/{index-Bb08wK-l.js → index-C2kxy-K_.js} +1 -1
  18. package/inspector/dist/assets/{index-DykDNrYm.js → index-CTECF5uX.js} +1 -1
  19. package/inspector/dist/assets/index-CtnGrZvu.js +217 -0
  20. package/inspector/dist/assets/{index-BrCwDkJZ.js → index-DBHgQG4M.js} +1 -1
  21. package/inspector/dist/assets/{index-CQ2XKqmD.js → index-DfqhDHp9.js} +1 -1
  22. package/inspector/dist/assets/index-DjMx3a5H.css +1 -0
  23. package/inspector/dist/assets/{index-yGZsomcy.js → index-DmY7gYaR.js} +1 -1
  24. package/inspector/dist/assets/{index-CaKmxcD6.js → index-Dq3QrDLb.js} +1 -1
  25. package/inspector/dist/assets/{index-oIQqfbwm.js → index-Fd3s8yys.js} +1 -1
  26. package/inspector/dist/assets/{index-Bs1U0rWW.js → index-Rmy9pam8.js} +1 -1
  27. package/inspector/dist/assets/{index-CUGfxoYt.js → index-VxlzgVIX.js} +1 -1
  28. package/inspector/dist/assets/{index-CReepirH.js → index-o4xPJnqZ.js} +1 -1
  29. package/inspector/dist/assets/{index-BKcOp35r.js → index-sSEE6ykA.js} +1 -1
  30. package/inspector/dist/index.html +2 -2
  31. package/package.json +1 -1
  32. package/inspector/dist/assets/index-Ba8VU6Wz.js +0 -217
  33. package/inspector/dist/assets/index-D4_mLTSH.css +0 -1
@@ -1,5 +1,6 @@
1
1
  import path from "node:path";
2
2
  import chalk from "chalk";
3
+ import { absolutePath } from "../config.js";
3
4
  import Table from "cli-table3";
4
5
  import { discoverWorkspaces, forgetProject, startProject, stopProject, tildify, } from "../inspector/instances.js";
5
6
  /**
@@ -43,7 +44,7 @@ const status = (e) => {
43
44
  /** Match a ref against known workspaces; `needRoot` excludes rootless live instances (start/forget need a root). */
44
45
  async function resolveRef(ref, needRoot = true) {
45
46
  const all = (await discoverWorkspaces()).filter((e) => e.root || !needRoot);
46
- const abs = path.resolve(ref);
47
+ const abs = absolutePath(ref);
47
48
  const port = /^\d+$/.test(ref) ? `http://localhost:${ref}` : `http://${ref.replace(/^https?:\/\//, "")}`;
48
49
  const hits = all.filter((e) => e.root === abs ||
49
50
  e.root === ref ||
package/dist/cli/ui.js CHANGED
@@ -4,7 +4,7 @@ import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { spawn, spawnSync } from "node:child_process";
6
6
  import chalk from "chalk";
7
- import { resolveProject } from "../config.js";
7
+ import { absolutePath, resolveProject } from "../config.js";
8
8
  import { selfCommand } from "../inspector/self-command.js";
9
9
  import { RESTART_EXIT_CODE, startInspector } from "../inspector/server.js";
10
10
  /**
@@ -52,7 +52,7 @@ export async function runUi(cwd, opts) {
52
52
  return superviseUi(opts);
53
53
  const staticDir = ensureBuilt();
54
54
  const project = await resolveProject(cwd);
55
- const outputDir = opts.output ? path.resolve(cwd, opts.output) : project.outputDir;
55
+ const outputDir = opts.output ? absolutePath(opts.output, cwd) : project.outputDir;
56
56
  // Port precedence: --port flag > kraftwerk.yml `port` > 1981.
57
57
  const port = opts.port ? Number(opts.port) : (project.config.port ?? 1981);
58
58
  await startInspector({ outputDir, staticDir, port, projectRoot: project.root });
package/dist/config.d.ts CHANGED
@@ -11,6 +11,7 @@
11
11
  *
12
12
  * name: my-project # display name (inspector header, "environment")
13
13
  * icon: "⚡" # emoji shown as inspector favicon
14
+ * color: "#c2410c" # accent for this workspace in the switcher (default: derived from the root)
14
15
  * port: 1981 # port `kraftwerk ui` listens on
15
16
  * workflows: src/workflows # workflows root, relative to the file
16
17
  * output: output # run-artifact directory, relative to the file
@@ -88,6 +89,8 @@ export interface ProjectConfig {
88
89
  name?: string;
89
90
  /** Emoji used as the inspector favicon (browser-tab icon). */
90
91
  icon?: string;
92
+ /** Accent colour (#rgb / #rrggbb) for this workspace in the switcher. Default: derived from the root path. */
93
+ color?: string;
91
94
  /** Port `kraftwerk ui` listens on. Default: 1981 (CLI --port wins). */
92
95
  port?: number;
93
96
  /** Workflows root relative to the project root. */
@@ -120,6 +123,14 @@ export interface Project {
120
123
  outputDir: string;
121
124
  }
122
125
  /** True when the path exists and is a directory. */
126
+ /**
127
+ * A user-supplied path as an absolute one: a leading `~` is the home
128
+ * directory (shells expand it, JSON bodies and quoted args do not), then
129
+ * path.resolve. Every path that gets stored or compared — registry roots,
130
+ * `--output`, project refs — goes through here so nothing ever persists
131
+ * `~/…` or `<cwd>/~/…`.
132
+ */
133
+ export declare function absolutePath(p: string, from?: string): string;
123
134
  export declare const isDir: (p: string) => Promise<boolean>;
124
135
  /**
125
136
  * Resolve the project for a cwd: walk up until a kraftwerk.yml or a
package/dist/config.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { readFile, stat } from "node:fs/promises";
2
+ import os from "node:os";
2
3
  import path from "node:path";
3
4
  import { parse } from "yaml";
4
5
  /**
@@ -14,6 +15,7 @@ import { parse } from "yaml";
14
15
  *
15
16
  * name: my-project # display name (inspector header, "environment")
16
17
  * icon: "⚡" # emoji shown as inspector favicon
18
+ * color: "#c2410c" # accent for this workspace in the switcher (default: derived from the root)
17
19
  * port: 1981 # port `kraftwerk ui` listens on
18
20
  * workflows: src/workflows # workflows root, relative to the file
19
21
  * output: output # run-artifact directory, relative to the file
@@ -63,6 +65,18 @@ export function gitignoreHas(text, entry) {
63
65
  }
64
66
  const exists = async (p) => !!(await stat(p).catch(() => null));
65
67
  /** True when the path exists and is a directory. */
68
+ /**
69
+ * A user-supplied path as an absolute one: a leading `~` is the home
70
+ * directory (shells expand it, JSON bodies and quoted args do not), then
71
+ * path.resolve. Every path that gets stored or compared — registry roots,
72
+ * `--output`, project refs — goes through here so nothing ever persists
73
+ * `~/…` or `<cwd>/~/…`.
74
+ */
75
+ export function absolutePath(p, from = process.cwd()) {
76
+ if (p === "~" || p.startsWith("~/") || p.startsWith("~\\"))
77
+ p = path.join(os.homedir(), p.slice(1));
78
+ return path.resolve(from, p);
79
+ }
66
80
  export const isDir = async (p) => (await stat(p).catch(() => null))?.isDirectory() ?? false;
67
81
  async function findConfigFile(dir) {
68
82
  for (const name of CONFIG_FILENAMES) {
@@ -113,7 +127,7 @@ export async function resolveProject(cwd) {
113
127
  const root = gitFallback ?? start;
114
128
  return { root, config: {}, outputDir: path.join(root, "output") };
115
129
  }
116
- const KNOWN_KEYS = ["name", "icon", "port", "workflows", "output", "knowledge", "agents", "skills", "switcher", "git", "repos"];
130
+ const KNOWN_KEYS = ["name", "icon", "color", "port", "workflows", "output", "knowledge", "agents", "skills", "switcher", "git", "repos"];
117
131
  async function loadConfig(configPath) {
118
132
  let raw;
119
133
  try {
@@ -138,6 +152,11 @@ async function loadConfig(configPath) {
138
152
  throw new Error(`${path.basename(configPath)}: port must be an integer`);
139
153
  }
140
154
  }
155
+ else if (key === "color") {
156
+ if (typeof config[key] !== "string" || !/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(config[key])) {
157
+ throw new Error(`${path.basename(configPath)}: color must be a hex colour like "#c2410c"`);
158
+ }
159
+ }
141
160
  else if (key === "switcher") {
142
161
  validateSwitcher(configPath, config[key]);
143
162
  }
@@ -15,6 +15,10 @@ export interface DiscoveredInstance {
15
15
  name: string;
16
16
  url: string;
17
17
  icon?: string;
18
+ /** kraftwerk.yml `color`, if set. */
19
+ color?: string;
20
+ /** True when the name comes from kraftwerk.yml (false = folder name). */
21
+ named?: boolean;
18
22
  live: true;
19
23
  root?: string;
20
24
  /** Server process id (what to signal to stop it). */
@@ -29,6 +33,10 @@ export interface WorkspaceEntry {
29
33
  /** Where the inspector answers (live) or would answer once started. */
30
34
  url: string;
31
35
  icon?: string;
36
+ /** kraftwerk.yml `color`, if set; the UI derives one from the root otherwise. */
37
+ color?: string;
38
+ /** True when the name comes from kraftwerk.yml, false when it is just the folder name. */
39
+ named?: boolean;
32
40
  live: boolean;
33
41
  root?: string;
34
42
  /** Root with ~ for the home dir — what the UI prints. */
@@ -3,7 +3,7 @@ import { createHash } from "node:crypto";
3
3
  import fsSync, { promises as fs } from "node:fs";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
- import { isDir, resolveProject } from "../config.js";
6
+ import { absolutePath, isDir, resolveProject } from "../config.js";
7
7
  import { selfCommand } from "./self-command.js";
8
8
  /**
9
9
  * Two registries under ~/.kraftwerk, two lifecycles:
@@ -31,7 +31,7 @@ const LOGS_DIR = path.join(HOME, "logs");
31
31
  const selfFile = () => path.join(INSTANCES_DIR, `${process.pid}.json`);
32
32
  let selfPort = null;
33
33
  let selfRoot = null;
34
- const projectKey = (root) => createHash("sha1").update(path.resolve(root)).digest("hex").slice(0, 16);
34
+ const projectKey = (root) => createHash("sha1").update(absolutePath(root)).digest("hex").slice(0, 16);
35
35
  const projectFile = (root) => path.join(PROJECTS_DIR, `${projectKey(root)}.json`);
36
36
  /** ~/… for display. */
37
37
  export const tildify = (p) => {
@@ -42,7 +42,7 @@ export const tildify = (p) => {
42
42
  /** Write this instance's registry file (call once the server listens). */
43
43
  export async function registerInstance(port, root) {
44
44
  selfPort = port;
45
- selfRoot = path.resolve(root);
45
+ selfRoot = absolutePath(root);
46
46
  try {
47
47
  await fs.mkdir(INSTANCES_DIR, { recursive: true });
48
48
  const rec = { pid: process.pid, port, startedAt: new Date().toISOString(), root: selfRoot };
@@ -86,6 +86,8 @@ async function probeAt(host, port) {
86
86
  name: meta.projectName || `localhost:${port}`,
87
87
  url: `http://localhost:${port}`,
88
88
  icon: meta.projectIcon || undefined,
89
+ color: meta.projectColor || undefined,
90
+ named: typeof meta.projectNamed === "boolean" ? meta.projectNamed : undefined,
89
91
  live: true,
90
92
  };
91
93
  }
@@ -151,7 +153,7 @@ async function readProject(file) {
151
153
  }
152
154
  /** Upsert the project record for a root (call on every inspector start). */
153
155
  export async function registerProject(root) {
154
- const abs = path.resolve(root);
156
+ const abs = absolutePath(root);
155
157
  const now = new Date().toISOString();
156
158
  try {
157
159
  await fs.mkdir(PROJECTS_DIR, { recursive: true });
@@ -232,7 +234,7 @@ export async function forgetProject(root) {
232
234
  * so without this check a stale record would wear its parent's identity.
233
235
  */
234
236
  async function describeRoot(root) {
235
- const fallback = { name: path.basename(root), port: 1981, exists: false };
237
+ const fallback = { name: path.basename(root), named: false, port: 1981, exists: false };
236
238
  if (!(await isDir(root)))
237
239
  return fallback;
238
240
  try {
@@ -241,7 +243,9 @@ async function describeRoot(root) {
241
243
  return fallback;
242
244
  return {
243
245
  name: project.config.name ?? path.basename(project.root),
246
+ named: !!project.config.name,
244
247
  icon: project.config.icon || undefined,
248
+ color: project.config.color || undefined,
245
249
  port: project.config.port ?? 1981,
246
250
  exists: true,
247
251
  };
@@ -278,15 +282,29 @@ export async function discoverWorkspaces() {
278
282
  }
279
283
  if (running) {
280
284
  consumed.add(running);
281
- return { ...base, name: running.name, url: running.url, icon: running.icon, live: true, exists: true };
285
+ // An instance from before 0.38 answers without color/named — read them from its root.
286
+ if (running.named === undefined)
287
+ d = await describeRoot(p.root);
288
+ return {
289
+ ...base,
290
+ name: running.name,
291
+ url: running.url,
292
+ icon: running.icon,
293
+ color: running.color ?? d?.color,
294
+ named: running.named ?? d?.named,
295
+ live: true,
296
+ exists: true,
297
+ };
282
298
  }
283
- return { ...base, name: d.name, url: `http://localhost:${d.port}`, icon: d.icon, live: false, exists: d.exists };
299
+ return { ...base, name: d.name, url: `http://localhost:${d.port}`, icon: d.icon, color: d.color, named: d.named, live: false, exists: d.exists };
284
300
  }))).concat(await Promise.all(live
285
301
  .filter((i) => !consumed.has(i))
286
302
  .map(async (i) => ({
287
303
  name: i.name,
288
304
  url: i.url,
289
305
  icon: i.icon,
306
+ color: i.color,
307
+ named: i.named,
290
308
  live: true,
291
309
  root: i.root,
292
310
  rootLabel: i.root ? tildify(i.root) : undefined,
@@ -335,6 +353,8 @@ export async function listWorkspacesDetailed() {
335
353
  all.unshift({
336
354
  name: d.name,
337
355
  icon: d.icon,
356
+ color: d.color,
357
+ named: d.named,
338
358
  url: `http://localhost:${selfPort}`,
339
359
  live: true,
340
360
  root: selfRoot,
@@ -395,7 +415,7 @@ export async function listWorkspacesDetailed() {
395
415
  * straight to it.
396
416
  */
397
417
  export async function startProject(root) {
398
- const abs = path.resolve(root);
418
+ const abs = absolutePath(root);
399
419
  const { name, port, exists } = await describeRoot(abs);
400
420
  if (!exists)
401
421
  return { ok: false, error: `not a project directory (missing or moved): ${abs}` };
@@ -464,12 +484,12 @@ export async function startProject(root) {
464
484
  * that registered no root. Waits up to ~5s for the port to go quiet.
465
485
  */
466
486
  export async function stopProject(target) {
467
- if (target.root && path.resolve(target.root) === selfRoot) {
487
+ if (target.root && absolutePath(target.root) === selfRoot) {
468
488
  return { ok: false, error: "that is this workspace — stop it from its own terminal or pid" };
469
489
  }
470
490
  cache = wsCache = null;
471
491
  const live = await discoverInstances();
472
- const inst = live.find((i) => (target.root && i.root === path.resolve(target.root)) || (target.url && i.url === target.url));
492
+ const inst = live.find((i) => (target.root && i.root === absolutePath(target.root)) || (target.url && i.url === target.url));
473
493
  if (!inst)
474
494
  return { ok: false, error: "not running" };
475
495
  try {
@@ -213,6 +213,8 @@ async function handleApi(req, res, url) {
213
213
  restartable: supervised(),
214
214
  projectName,
215
215
  projectIcon: project?.config.icon ?? "",
216
+ projectColor: project?.config.color ?? "",
217
+ projectNamed: !!project?.config.name,
216
218
  projectRoot: project?.root ?? getProjectRoot(),
217
219
  projectRootLabel: tildify(project?.root ?? getProjectRoot()),
218
220
  git: (project?.config.git && project.config.git.enabled !== false) === true,
@@ -20,6 +20,8 @@ export interface SaveSettingsInput {
20
20
  /** Omitted = untouched; empty string = remove the key. */
21
21
  name?: string;
22
22
  icon?: string;
23
+ /** Omitted = untouched; empty string = remove the key. Hex colour otherwise. */
24
+ color?: string;
23
25
  /** Omitted = untouched; empty list = remove the key. */
24
26
  switcher?: SwitcherEntry[];
25
27
  /** Omitted = untouched. See cleanGit for how the block is written. */
@@ -94,6 +94,13 @@ export async function saveSettings(input) {
94
94
  throw new Error("name must be a string");
95
95
  if (input.icon !== undefined && typeof input.icon !== "string")
96
96
  throw new Error("icon must be a string");
97
+ if (input.color !== undefined) {
98
+ if (typeof input.color !== "string")
99
+ throw new Error("color must be a string");
100
+ if (input.color.trim() && !/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(input.color.trim())) {
101
+ throw new Error('color must be a hex colour like "#c2410c"');
102
+ }
103
+ }
97
104
  const switcher = input.switcher === undefined ? undefined : cleanSwitcher(input.switcher);
98
105
  const project = await resolveProject(getProjectRoot());
99
106
  const configPath = project.configPath ?? path.join(project.root, "kraftwerk.yml");
@@ -111,6 +118,7 @@ export async function saveSettings(input) {
111
118
  };
112
119
  setOrDelete("name", input.name?.trim());
113
120
  setOrDelete("icon", input.icon?.trim());
121
+ setOrDelete("color", input.color?.trim());
114
122
  setOrDelete("switcher", switcher);
115
123
  if (input.git !== undefined) {
116
124
  const git = cleanGit(input.git);