@antelopejs/dms 0.5.0 → 0.5.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.
Files changed (64) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/dist/implementations/dms/page.js +69 -22
  3. package/dist/implementations/dms/page.js.map +1 -1
  4. package/dist/index.js +2 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/pages/settings/users/member-invite-form.d.ts +5 -0
  7. package/dist/pages/settings/users/member-invite-form.js +11 -2
  8. package/dist/pages/settings/users/member-invite-form.js.map +1 -1
  9. package/dist/pages/settings/users/members.d.ts +2 -0
  10. package/dist/pages/settings/users/members.js +11 -0
  11. package/dist/pages/settings/users/members.js.map +1 -1
  12. package/dist/realtime/index.js +2 -0
  13. package/dist/realtime/index.js.map +1 -1
  14. package/dist/realtime/sessions.d.ts +3 -0
  15. package/dist/realtime/sessions.js +5 -0
  16. package/dist/realtime/sessions.js.map +1 -1
  17. package/dist/realtime/sse.d.ts +1 -1
  18. package/dist/realtime/sse.js +9 -3
  19. package/dist/realtime/sse.js.map +1 -1
  20. package/dist/routes/realtime.js +8 -3
  21. package/dist/routes/realtime.js.map +1 -1
  22. package/dist/test/helpers/reload-host.d.ts +7 -2
  23. package/dist/test/helpers/reload-host.js +23 -4
  24. package/dist/test/helpers/reload-host.js.map +1 -1
  25. package/dist/test/reload-host/index.js +27 -1
  26. package/dist/test/reload-host/index.js.map +1 -1
  27. package/dist/test/reload-host/package.json +1 -1
  28. package/dist/test/unit/implementations/dms/missing-category.test.d.ts +1 -0
  29. package/dist/test/unit/implementations/dms/missing-category.test.js +174 -0
  30. package/dist/test/unit/implementations/dms/missing-category.test.js.map +1 -0
  31. package/dist/test/unit/interfaces/dms/database-initialized-hook.test.d.ts +1 -0
  32. package/dist/test/unit/interfaces/dms/database-initialized-hook.test.js +92 -0
  33. package/dist/test/unit/interfaces/dms/database-initialized-hook.test.js.map +1 -0
  34. package/dist/test/unit/interfaces/dms/invite-extension.test.js +59 -0
  35. package/dist/test/unit/interfaces/dms/invite-extension.test.js.map +1 -1
  36. package/dist/test/unit/interfaces/dms/module-reload-registrations.test.js +2 -2
  37. package/dist/test/unit/interfaces/dms/module-reload-registrations.test.js.map +1 -1
  38. package/dist/test/unit/interfaces/dms/page-extension-ownership.test.d.ts +1 -0
  39. package/dist/test/unit/interfaces/dms/page-extension-ownership.test.js +153 -0
  40. package/dist/test/unit/interfaces/dms/page-extension-ownership.test.js.map +1 -0
  41. package/dist/test/unit/pages/settings/invite-availability.test.d.ts +1 -0
  42. package/dist/test/unit/pages/settings/invite-availability.test.js +112 -0
  43. package/dist/test/unit/pages/settings/invite-availability.test.js.map +1 -0
  44. package/dist/test/unit/realtime/stop-closes-streams.test.d.ts +1 -0
  45. package/dist/test/unit/realtime/stop-closes-streams.test.js +99 -0
  46. package/dist/test/unit/realtime/stop-closes-streams.test.js.map +1 -0
  47. package/dist/test/unit/skills/dms-dev-script.test.d.ts +1 -0
  48. package/dist/test/unit/skills/dms-dev-script.test.js +137 -0
  49. package/dist/test/unit/skills/dms-dev-script.test.js.map +1 -0
  50. package/dist/validation/auth.schema.d.ts +2 -2
  51. package/frontend-vue/dms.frontend.ts +4 -3
  52. package/frontend-vue/layers/dms-core/app/composables/realtime/useUserRealtime.ts +116 -44
  53. package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +2 -0
  54. package/frontend-vue/layers/dms-ui/app/build/components/table/Actions.vue +18 -8
  55. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +12 -0
  56. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/custom-button.ts +4 -0
  57. package/frontend-vue/layers/dms-ui/app/plugins/shortcuts.ts +1 -1
  58. package/frontend-vue/layers/dms-ui/app/types/shortcuts-aggregated.d.ts +8 -0
  59. package/frontend-vue/tests/realtime-reconnect.test.ts +215 -0
  60. package/frontend-vue/tests/shortcuts-aggregate.test.ts +41 -0
  61. package/frontend-vue/vitest.config.ts +4 -0
  62. package/package.json +4 -4
  63. package/skills/dms-dev/SKILL.md +28 -16
  64. package/skills/dms-dev/scripts/dms-dev.sh +275 -83
@@ -0,0 +1,215 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { nextTick, ref, watch } from "vue";
3
+ import type { UserRealtimeApi } from "../layers/dms-core/app/composables/realtime/useUserRealtime";
4
+
5
+ const BASE_URL = "http://api";
6
+ const USER_STREAM_URL = `${BASE_URL}/api/realtime/user`;
7
+ const SUBSCRIBE_URL_PREFIX = `${BASE_URL}/api/realtime/subscribe/`;
8
+ const SESSION_HEADER = "x-realtime-session";
9
+ const PAGE_ID = "orders";
10
+ const INITIAL_DELAY_MS = 500;
11
+ const SECOND_DELAY_MS = 1000;
12
+ const LONG_WAIT_MS = 120_000;
13
+
14
+ interface FakeStream {
15
+ signal: AbortSignal;
16
+ end(): void;
17
+ isOpen(): boolean;
18
+ }
19
+
20
+ interface PageSubscription {
21
+ pageId: string;
22
+ sessionId: string;
23
+ }
24
+
25
+ interface FakeServer {
26
+ streams: FakeStream[];
27
+ subscriptions: PageSubscription[];
28
+ maxOpenStreams: number;
29
+ isDown: boolean;
30
+ }
31
+
32
+ const encoder = new TextEncoder();
33
+
34
+ const helloFrame = (sessionId: string): Uint8Array =>
35
+ encoder.encode(`event: hello\ndata: ${JSON.stringify({ sessionId })}\n\n`);
36
+
37
+ const openStreamCount = (server: FakeServer): number =>
38
+ server.streams.filter((stream) => stream.isOpen()).length;
39
+
40
+ const openUserStream = (server: FakeServer, signal: AbortSignal) => {
41
+ let controller!: ReadableStreamDefaultController<Uint8Array>;
42
+ let isOpen = true;
43
+ const body = new ReadableStream<Uint8Array>({
44
+ start(streamController) {
45
+ controller = streamController;
46
+ },
47
+ cancel() {
48
+ isOpen = false;
49
+ },
50
+ });
51
+ signal.addEventListener("abort", () => {
52
+ isOpen = false;
53
+ });
54
+ server.streams.push({
55
+ signal,
56
+ end: () => {
57
+ isOpen = false;
58
+ controller.close();
59
+ },
60
+ isOpen: () => isOpen && !signal.aborted,
61
+ });
62
+ server.maxOpenStreams = Math.max(
63
+ server.maxOpenStreams,
64
+ openStreamCount(server),
65
+ );
66
+ controller.enqueue(helloFrame(`session-${server.streams.length}`));
67
+ return { ok: true, status: 200, body };
68
+ };
69
+
70
+ const installFetch = (server: FakeServer): void => {
71
+ globalThis.fetch = vi.fn(async (url: string, init?: RequestInit) => {
72
+ if (url === USER_STREAM_URL) {
73
+ if (server.isDown) throw new Error("connection refused");
74
+ return openUserStream(server, init!.signal!);
75
+ }
76
+ const headers = init?.headers as Record<string, string>;
77
+ server.subscriptions.push({
78
+ pageId: url.slice(SUBSCRIBE_URL_PREFIX.length),
79
+ sessionId: headers[SESSION_HEADER]!,
80
+ });
81
+ return { ok: true, status: 200 };
82
+ }) as unknown as typeof fetch;
83
+ };
84
+
85
+ const settle = async (): Promise<void> => {
86
+ await vi.advanceTimersByTimeAsync(0);
87
+ await nextTick();
88
+ await vi.advanceTimersByTimeAsync(0);
89
+ };
90
+
91
+ const realFetch = globalThis.fetch;
92
+ let loggedIn = ref(false);
93
+ let server: FakeServer;
94
+
95
+ const loadApi = async (): Promise<UserRealtimeApi> => {
96
+ const module = await import(
97
+ "../layers/dms-core/app/composables/realtime/useUserRealtime"
98
+ );
99
+ return module.useUserRealtime();
100
+ };
101
+
102
+ const lastStream = (): FakeStream => server.streams[server.streams.length - 1]!;
103
+
104
+ beforeEach(() => {
105
+ vi.resetModules();
106
+ vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });
107
+ vi.stubEnv("SSR", false);
108
+ vi.stubGlobal("ref", ref);
109
+ vi.stubGlobal("watch", watch);
110
+ vi.stubGlobal("useDmsState", (_key: string, init: () => unknown) =>
111
+ ref(init()),
112
+ );
113
+ vi.stubGlobal("useDmsRuntimeConfig", () => ({
114
+ public: { dms: { baseURL: BASE_URL } },
115
+ }));
116
+ vi.stubGlobal("useUserSession", () => ({ loggedIn }));
117
+ vi.stubGlobal("useSessionRecovery", () => ({
118
+ refreshSession: async () => true,
119
+ redirectToAuth: async () => {},
120
+ }));
121
+ server = { streams: [], subscriptions: [], maxOpenStreams: 0, isDown: false };
122
+ installFetch(server);
123
+ // Fresh per test: the singleton of a previous test keeps watching its own.
124
+ loggedIn = ref(true);
125
+ });
126
+
127
+ afterEach(async () => {
128
+ loggedIn.value = false;
129
+ await settle();
130
+ globalThis.fetch = realFetch;
131
+ vi.useRealTimers();
132
+ vi.unstubAllGlobals();
133
+ vi.unstubAllEnvs();
134
+ });
135
+
136
+ describe("useUserRealtime reconnection", () => {
137
+ it("reconnects with backoff when the server ends the stream, then resubscribes the page", async () => {
138
+ const api = await loadApi();
139
+ await settle();
140
+ await api.setPage(PAGE_ID);
141
+ expect(api.sessionId.value).toBe("session-1");
142
+ expect(server.subscriptions).toEqual([
143
+ { pageId: PAGE_ID, sessionId: "session-1" },
144
+ ]);
145
+
146
+ lastStream().end();
147
+ await settle();
148
+ expect(api.sessionId.value).toBeUndefined();
149
+ await vi.advanceTimersByTimeAsync(INITIAL_DELAY_MS - 1);
150
+ expect(server.streams).toHaveLength(1);
151
+
152
+ await vi.advanceTimersByTimeAsync(1);
153
+ await settle();
154
+ expect(server.streams).toHaveLength(2);
155
+ expect(api.sessionId.value).toBe("session-2");
156
+ expect(server.subscriptions).toEqual([
157
+ { pageId: PAGE_ID, sessionId: "session-1" },
158
+ { pageId: PAGE_ID, sessionId: "session-2" },
159
+ ]);
160
+ });
161
+
162
+ it("backs off further while the server stays unreachable", async () => {
163
+ await loadApi();
164
+ await settle();
165
+ server.isDown = true;
166
+
167
+ lastStream().end();
168
+ await settle();
169
+ await vi.advanceTimersByTimeAsync(INITIAL_DELAY_MS);
170
+ await settle();
171
+ expect(vi.mocked(globalThis.fetch)).toHaveBeenCalledTimes(2);
172
+
173
+ server.isDown = false;
174
+ await vi.advanceTimersByTimeAsync(SECOND_DELAY_MS - 1);
175
+ expect(server.streams).toHaveLength(1);
176
+ await vi.advanceTimersByTimeAsync(1);
177
+ await settle();
178
+ expect(server.streams).toHaveLength(2);
179
+ expect(lastStream().isOpen()).toBe(true);
180
+ });
181
+
182
+ it("does not reconnect after an intentional close", async () => {
183
+ await loadApi();
184
+ await settle();
185
+ const stream = lastStream();
186
+
187
+ loggedIn.value = false;
188
+ await settle();
189
+ expect(stream.signal.aborted).toBe(true);
190
+
191
+ await vi.advanceTimersByTimeAsync(LONG_WAIT_MS);
192
+ await settle();
193
+ expect(server.streams).toHaveLength(1);
194
+ });
195
+
196
+ it("never holds more than one stream at a time", async () => {
197
+ await loadApi();
198
+ await settle();
199
+
200
+ for (let cycle = 0; cycle < 3; cycle += 1) {
201
+ lastStream().end();
202
+ await settle();
203
+ await vi.advanceTimersByTimeAsync(INITIAL_DELAY_MS);
204
+ await settle();
205
+ }
206
+ loggedIn.value = false;
207
+ await settle();
208
+ loggedIn.value = true;
209
+ await settle();
210
+
211
+ expect(server.streams).toHaveLength(5);
212
+ expect(server.maxOpenStreams).toBe(1);
213
+ expect(openStreamCount(server)).toBe(1);
214
+ });
215
+ });
@@ -0,0 +1,41 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const finderShortcut = {
4
+ id: "finder.open",
5
+ keys: ["enter"],
6
+ description: "Open",
7
+ };
8
+
9
+ vi.mock("#shortcuts-aggregated", () => ({
10
+ default: [
11
+ {
12
+ component: "Tab",
13
+ shortcuts: [{ id: "tab.next", keys: ["ctrl", "tab"], description: "" }],
14
+ },
15
+ { component: "Finder", shortcuts: [finderShortcut] },
16
+ ],
17
+ }));
18
+
19
+ describe("the shortcuts plugin", () => {
20
+ afterEach(() => {
21
+ vi.unstubAllGlobals();
22
+ });
23
+
24
+ it("registers every module's registry from the aggregate, not only dms-ui's", async () => {
25
+ const registered: Array<[string, unknown]> = [];
26
+ vi.stubGlobal("defineDmsPlugin", (setup: () => void) => setup);
27
+ vi.stubGlobal("useShortcutRegistry", () => ({
28
+ registerShortcut: (component: string, shortcut: unknown) =>
29
+ registered.push([component, shortcut]),
30
+ }));
31
+ const { default: setup } = await import(
32
+ "../layers/dms-ui/app/plugins/shortcuts"
33
+ );
34
+ (setup as () => void)();
35
+ expect(registered.map(([component]) => component)).toEqual([
36
+ "Tab",
37
+ "Finder",
38
+ ]);
39
+ expect(registered[1]?.[1]).toEqual(finderShortcut);
40
+ });
41
+ });
@@ -9,6 +9,10 @@ export default defineConfig({
9
9
  "#dms-core": resolve(__dirname, "layers/dms-core"),
10
10
  "#dms-ui": resolve(__dirname, "layers/dms-ui"),
11
11
  "#dms-layout": resolve(__dirname, "layers/dms-layout"),
12
+ "#shortcuts-aggregated": resolve(
13
+ __dirname,
14
+ "layers/dms-ui/app/config/shortcuts-registry.ts",
15
+ ),
12
16
  },
13
17
  },
14
18
  esbuild: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antelopejs/dms",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/AntelopeJS/dms.git"
@@ -36,7 +36,7 @@
36
36
  "@antelopejs/interface-data-api": ">=0.1.9 <1.0.0",
37
37
  "@antelopejs/interface-database": ">=0.1.8 <1.0.0",
38
38
  "@antelopejs/interface-database-decorators": ">=0.1.7 <1.0.0",
39
- "@antelopejs/interface-dms": ">=0.3.1 <0.4.0",
39
+ "@antelopejs/interface-dms": ">=0.3.3 <0.4.0",
40
40
  "@antelopejs/interface-email": ">=0.0.5 <1.0.0",
41
41
  "@antelopejs/interface-file-storage": ">=0.1.3 <1.0.0",
42
42
  "archiver": "^7.0.1",
@@ -53,7 +53,7 @@
53
53
  },
54
54
  "devDependencies": {
55
55
  "@antelopejs/core": ">=1.10.1 <2",
56
- "@antelopejs/tooling-configs": ">=0.0.6 <1.0.0",
56
+ "@antelopejs/tooling-configs": ">=0.0.7 <1.0.0",
57
57
  "@types/archiver": "^6.0.4",
58
58
  "@types/chai": "^4.3.20",
59
59
  "@types/jsonwebtoken": "^9.0.10",
@@ -107,7 +107,7 @@
107
107
  "dev": "ajs project run --inspect -w -p playground",
108
108
  "frontend:dev": "pnpm --dir playground frontend:dev",
109
109
  "lint:fix": "oxlint --fix --max-warnings=1000000 && oxfmt . && pnpm --dir frontend-vue lint:fix",
110
- "lint": "oxlint && oxfmt --check . && pnpm --dir frontend-vue lint",
110
+ "lint": "oxlint && oxfmt --check . && pnpm --dir frontend-vue lint && antelopejs-check-interface-ranges",
111
111
  "typecheck": "pnpm --filter @antelopejs/interface-dms build && tsc --noEmit",
112
112
  "test": "pnpm run build && ajs module test .",
113
113
  "test:unit": "pnpm run build && DMS_TEST_DIR=dist/test/unit ajs module test .",
@@ -3,7 +3,7 @@ name: dms-dev
3
3
  description: Starts, stops, or restarts the AntelopeJS DMS backend and Inertia frontend.
4
4
  category: tooling
5
5
  tags: [dev-server, restart, backend, inertia, workflow]
6
- allowed-tools: Bash(bash:*), Bash(pkill:*), Bash(pgrep:*), Bash(tail:*), Bash(curl:*), Read, Write
6
+ allowed-tools: Bash(bash:*), Bash(pgrep:*), Bash(tail:*), Bash(curl:*), Read, Write
7
7
  ---
8
8
 
9
9
  # dms-dev
@@ -14,6 +14,15 @@ Owns the dev-server lifecycle for an AntelopeJS DMS project. Ships with the
14
14
  resolve the project path. For authoring see the sibling **dms**, **dms-module**,
15
15
  **dms-pages**, and **dms-auth** skills.
16
16
 
17
+ ## Never from inside the DMS
18
+
19
+ An agent that runs inside the DMS -- the dms-ai assistant, whose sidecar the backend
20
+ spawns, or any process started by these dev servers -- must **not** run `stop`, `restart`
21
+ or `start`: stopping the backend takes the agent down with it. Rely on hot reload (the
22
+ backend's `-w` watcher and the frontend's live layer resync) and, when a change needs a
23
+ full restart, ask the user to run it. The script refuses to stop a process tree that
24
+ contains the shell calling it.
25
+
17
26
  ## Choose the process manager
18
27
 
19
28
  In an Amp orb, use the repository's `.amp/services.yaml` with `amp orb services ensure`,
@@ -21,18 +30,18 @@ or `amp orb service start` for a one-off service. Inspect readiness with service
21
30
  HTTP checks; share the returned portal URL for browser review. The bundled detached-shell
22
31
  script is for local shells, not managed orbs: its processes do not survive orb updates.
23
32
 
24
- ## Why full restart, not hot reload
33
+ ## Hot reload first, restart when it cannot apply
25
34
 
26
35
  The DMS has two coupled processes, both started from the DMS backend package directory:
27
36
 
28
- - **Backend**: the AntelopeJS project — `ajs project dev -w` (`ajs project run` is the legacy alias). Listens on `http://localhost:5010`. Ready when the log emits `Server started, listening on http://localhost:5010`.
29
- - **Frontend**: the `@antelopejs/dms-frontend` loader — `ajs dms dev`. It auto-discovers the running backend via `.antelope/dev.json`, so `-b <backend-url>` is optional. Listens on `http://localhost:3001`. Ready when the log emits `Local: http://localhost:3001/`.
37
+ - **Backend**: the AntelopeJS project — `ajs project dev -w` (`ajs project run` is the legacy alias). Ready when the log emits `Server started, listening on <url>`; the real endpoint is recorded in the project's `.antelope/dev.json` (`servers.api.endpoints`), `http://localhost:5010` by default.
38
+ - **Frontend**: the `@antelopejs/dms-frontend` loader — `ajs dms dev`. It auto-discovers the running backend via `.antelope/dev.json`, so `-b <backend-url>` is optional. Ready when the log emits `Server ready on <url>` (`http://localhost:3001` by default).
30
39
 
31
40
  By default the script runs each via the project's `pnpm dev` / `pnpm frontend:dev` scripts — a convention our playground projects adopt that expands to the two commands above. Those script names aren't shipped by the DMS, so if a project starts its servers differently, override the actual commands with the `DMS_BACK_CMD` / `DMS_FRONT_CMD` env vars (the script reads them; readiness detection and process cleanup already cover `ajs`, `ajs dms`, and the loader's Node entry point directly).
32
41
 
33
42
  Order matters: start the backend first and wait for ready, **then** start the frontend. The frontend materializes the backend's layers into a `~/.antelopejs/dms-frontend/` workspace at startup; starting it before the backend is up produces stale or empty layer copies.
34
43
 
35
- The docs claim dev-mode watchers normally propagate edits (backend `-w` recompile, live layer resync into `~/.antelopejs/dms-frontend/`) — and they often do. But the failure modes are silent: `RegisterModule` can throw "already registered" on rebuild, and a missed resync serves a stale layer copy that *looks* like your bug. The documented troubleshooting recipe — and this skill's contract as an agent — is: **full restart both, backend first, before verifying any change**. A 60-second restart beats debugging a phantom.
44
+ Dev-mode watchers propagate most edits (backend `-w` recompile, live layer resync into `~/.antelopejs/dms-frontend/`). Their failure modes are silent, though: `RegisterModule` can throw "already registered" on rebuild, and a missed resync serves a stale layer copy that *looks* like your bug. So restart only when hot reload cannot apply the change -- a new module or dependency, a config edit, a backend log showing a failed rebuild, or a behaviour that still looks stale after the watcher reported the rebuild -- and then restart both, backend first. Never restart from inside the DMS (see above).
36
45
 
37
46
  ## Resolving the backend path (do this FIRST, eagerly)
38
47
 
@@ -71,27 +80,29 @@ bundled script lives at `scripts/dms-dev.sh` under it. Subcommands:
71
80
 
72
81
  | Command | Use when |
73
82
  |-------------------|-------------------------------------------------------------------------|
74
- | `restart` | After any source edit, or unconditionally if you don't know the state. |
83
+ | `restart` | When hot reload cannot apply a change, or the state is unknown. Never from inside the DMS. |
75
84
  | `start` | When you're sure nothing is running (fresh shell / after `stop`). |
76
- | `stop` | Before walking away, before destructive operations, on session cleanup. |
85
+ | `stop` | Before walking away, before destructive operations, on session cleanup. Never from inside the DMS. |
77
86
  | `status` | Quickly check what's running. |
78
87
  | `back-log [N]` | Read the last N (default 60) backend log lines. Use to diagnose boot. |
79
88
  | `front-log [N]` | Same for the frontend log. |
80
89
 
81
- The script writes the long-running server logs to `/tmp/dms-back.log` and `/tmp/dms-front.log`. The `Bash` tool invocation completes as soon as both servers print their ready strings (or after a 180 s timeout per server — worst case ~6 minutes for a restart; override with `DMS_DEV_TIMEOUT`). The servers themselves keep running detached via `setsid nohup` so they survive the Bash call exiting.
90
+ The script writes the long-running server logs to `/tmp/dms-back.log` and `/tmp/dms-front.log`. The `Bash` tool invocation completes as soon as both servers print their ready strings, and reports the URLs they really listen on. It fails fast when a server process exits before becoming ready, and otherwise after a 180 s timeout per server (override with `DMS_DEV_TIMEOUT`). The servers keep running detached in their own session (`setsid`, or perl's `POSIX::setsid` on macOS) so they survive the Bash call exiting.
91
+
92
+ `stop` and `restart` only touch the project in `DMS_BACK_DIR`: the pids the script starts are recorded in `DMS_BACK_DIR/.antelope/dms-dev.pid`, and without that file it only matches dev processes whose working directory or command line lies under `DMS_BACK_DIR`. Each process gets SIGTERM, then SIGKILL after `DMS_DEV_STOP_GRACE` seconds (default 10). Other projects' dev servers on the machine are never signalled.
82
93
 
83
94
  ## When to reach for the subcommands
84
95
 
85
- - The user edits any `.ts` or `.vue` file under the DMS backend package or under a DMS module's `src/` or `frontend-vue/` and then wants to test the change live. Run `restart` unconditionally — don't try hot reload.
86
- - The user says "restart the dms", "restart servers", "reload", or "test it now". Run `restart`.
96
+ - The user edits a `.ts` or `.vue` file under the DMS backend package or under a DMS module's `src/` or `frontend-vue/` and wants to test the change live. Let hot reload apply it first; run `restart` only when it cannot (see above). From inside the DMS, never run it.
97
+ - The user says "restart the dms", "restart servers", "reload", or "test it now". Run `restart` -- unless you run inside the DMS, in which case say so and let the user restart.
87
98
  - A browser smoke test fails in a way that looks like stale state. Run `restart` before diagnosing.
88
99
  - The user asks "is the dms running?". Run `status`.
89
- - A boot fails (backend never prints "Server started" or frontend never prints "Local:"). Print the relevant `back-log` / `front-log` tail to diagnose; common causes include `RegisterModule … already registered` (a previous stop failed — run `stop` then `restart`), port already in use (kill the listed PID), or a missing dependency (run `pnpm install` in the offending module).
90
- - **You are handing off to the user for manual testing** (e.g. a plan phase whose acceptance criterion is "user verifies in browser"). Run `restart` yourself first, wait for both ready strings, and only *then* tell the user what to click. Never ask the user to start the servers themselves — that's the whole reason this skill exists.
100
+ - A boot fails (backend never prints "Server started" or frontend never prints "Server ready on"). Print the relevant `back-log` / `front-log` tail to diagnose; common causes include `RegisterModule … already registered` (a previous stop failed — run `stop` then `restart`), port already in use (kill the listed PID), or a missing dependency (run `pnpm install` in the offending module).
101
+ - **You are handing off to the user for manual testing** (e.g. a plan phase whose acceptance criterion is "user verifies in browser"). Run `restart` yourself first, wait for both ready strings, and only *then* tell the user what to click. Never ask the user to start the servers themselves — that's the whole reason this skill exists. The one exception is an agent running inside the DMS, which already runs on live servers.
91
102
 
92
103
  ## Common pitfalls
93
104
 
94
- - **Zombie frontend processes.** The frontend CLI can spawn child processes that survive `pkill ajs`. The script's `stop` does an automatic second-pass kill via `pgrep | xargs kill -9` to catch them. If `start` reports servers already running, run `stop` and check that `status` shows `(none)` before starting again.
105
+ - **Zombie frontend processes.** The frontend CLI spawns child processes. The script's `stop` signals the whole session and process tree it started, and SIGKILLs whatever survives the grace period. If `start` reports servers already running, run `stop` and check that `status` shows `(none)` before starting again.
95
106
  - **Backend exits with "Module already registered".** Means a previous instance is still holding the registration. Run `stop` to clear it, then `restart`.
96
107
  - **Frontend's "auth redirect" 302** on `/modules/...` URLs is **normal** — it's the auth-protected admin routes. The frontend layer is loaded correctly even when curl returns 302. Don't restart on this alone; have the user log in.
97
108
  - **Don't run `start` or `restart` more than once concurrently** in a single session — the second invocation will hit a port/lock conflict. If multiple background Bash tasks are queued, use the foreground `restart`.
@@ -101,7 +112,8 @@ The script writes the long-running server logs to `/tmp/dms-back.log` and `/tmp/
101
112
 
102
113
  ## Don't
103
114
 
104
- - Don't try to hot-reload by editing files and refreshing the browser without `restart`. It will work intermittently and waste debugging cycles on phantom issues.
105
- - Don't `pkill -9 node` — it kills unrelated processes. The script targets specific patterns (`antelope-runner`, `ajs project run|dev`, `ajs dms dev`, and paths ending in `dms-frontend/dist/index.js dev`).
115
+ - Don't keep debugging a behaviour that looks stale after hot reload: that is the case for `restart` (from outside the DMS).
116
+ - Don't run `stop`, `restart` or `start` from an agent running inside the DMS (e.g. dms-ai): it would stop its own host.
117
+ - Don't `pkill` dev servers by name (`pkill -9 node`, `pkill -f antelope-runner`): other projects on the machine run the same processes. Use the script, which only signals the processes of `DMS_BACK_DIR`.
106
118
  - Don't hardcode a backend path into a session. Always resolve via memory-or-prompt so the skill works across projects.
107
- - Don't ask the user to start or restart the servers themselves before manual testing. You have the script — run it, wait for ready, then hand off. Asking the user to do it defeats the point of the skill and breaks the testing loop.
119
+ - Don't ask the user to start or restart the servers themselves before manual testing. You have the script — run it, wait for ready, then hand off. Asking the user to do it defeats the point of the skill and breaks the testing loop. (An agent inside the DMS is the exception: it never restarts its own host.)