@stravica-ai/digital-operator-pa 0.1.46 → 0.1.48
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/engineering-pa-core/docs/commands/onboard.md +19 -2
- package/dist/engineering-pa-scaffold/package.json +3 -2
- package/dist/engineering-pa-scaffold/src/commands/init-workspace-extension.mjs +37 -5
- package/dist/engineering-pa-scaffold/src/commands/init.mjs +7 -1
- package/dist/engineering-pa-scaffold/src/prompts.mjs +47 -3
- package/dist/engineering-pa-workspace/app/assets/theme.css +3 -2
- package/dist/engineering-pa-workspace/app/assets/theme.js +29 -6
- package/dist/engineering-pa-workspace/app/banner.js +14 -2
- package/dist/engineering-pa-workspace/app/delegations.html +1 -1
- package/dist/engineering-pa-workspace/app/home.html +1 -1
- package/dist/engineering-pa-workspace/app/index.html +1 -1
- package/dist/engineering-pa-workspace/app/knowledge-map.html +1 -1
- package/dist/engineering-pa-workspace/app/knowledge.html +1 -1
- package/dist/engineering-pa-workspace/app/notes.html +1 -1
- package/dist/engineering-pa-workspace/app/onboarding.html +1 -1
- package/dist/engineering-pa-workspace/app/profile.html +1 -1
- package/dist/engineering-pa-workspace/app/project-detail.html +1 -1
- package/dist/engineering-pa-workspace/app/projects.html +1 -1
- package/dist/engineering-pa-workspace/app/repos.html +1 -1
- package/dist/engineering-pa-workspace/app/schedules.html +1 -1
- package/dist/engineering-pa-workspace/app/secrets.html +1 -1
- package/dist/engineering-pa-workspace/app/staging.html +1 -1
- package/dist/engineering-pa-workspace/app/tasks.html +1 -1
- package/dist/engineering-pa-workspace/app/viewer.html +1 -1
- package/dist/engineering-pa-workspace/index.mjs +16 -0
- package/dist/engineering-pa-workspace/package.json +3 -2
- package/dist/engineering-pa-workspace/src/install-daemon.mjs +8 -19
- package/dist/engineering-pa-workspace/src/repo-root.mjs +100 -0
- package/dist/engineering-pa-workspace/src/server.mjs +12 -25
- package/dist/engineering-pa-workspace/src/status-mon/run.mjs +9 -8
- package/dist/engineering-pa-workspace/src/workspace-page-contract.mjs +98 -0
- package/dist/engineering-pa-workspace/src/ws.mjs +13 -1
- package/docs/what-it-can-do.md +4 -4
- package/package.json +3 -6
|
@@ -94,8 +94,25 @@ line to run; do not run it for them (their shell rc is not our file to write).
|
|
|
94
94
|
|
|
95
95
|
Ask if they want the local workspace started now. On yes, run
|
|
96
96
|
`pnpm exec pa workspace start` in the background and print the URL from
|
|
97
|
-
`operator/config/preferences.json:workspacePort` (default 4200).
|
|
98
|
-
|
|
97
|
+
`operator/config/preferences.json:workspacePort` (default 4200).
|
|
98
|
+
|
|
99
|
+
If the start fails because the configured port is already in use (the
|
|
100
|
+
operator has something else on 4200, or the previous session did not
|
|
101
|
+
shut down cleanly), do NOT stop the walkthrough. Instead:
|
|
102
|
+
|
|
103
|
+
1. Pick a free port at or above 4210 (probe 4210, 4211, 4212 ... in
|
|
104
|
+
order until one is free).
|
|
105
|
+
2. Retry `pnpm exec pa workspace start --port N` with that port.
|
|
106
|
+
3. On success, update `operator/config/preferences.json` with
|
|
107
|
+
`"workspacePort": N` so subsequent sessions start on the same port
|
|
108
|
+
without asking again. Preserve the rest of the file exactly, and
|
|
109
|
+
round-trip through `JSON.parse` / `JSON.stringify` with a two-space
|
|
110
|
+
indent to keep the on-disk shape stable.
|
|
111
|
+
4. Tell the operator you moved the workspace off 4200 and named the
|
|
112
|
+
port that stuck.
|
|
113
|
+
|
|
114
|
+
On no, note that `pnpm exec pa workspace start` is the verb whenever
|
|
115
|
+
they want it.
|
|
99
116
|
|
|
100
117
|
### 6. atlassianOffer
|
|
101
118
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stravica-ai/engineering-pa-scaffold",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "pa-init scaffolder + onboarding flows (solo, lead, member).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.mjs",
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
"pa-init": "bin/pa-init"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@stravica-ai/engineering-pa-core": "workspace:*"
|
|
14
|
+
"@stravica-ai/engineering-pa-core": "workspace:*",
|
|
15
|
+
"@stravica-ai/engineering-pa-workspace": "workspace:*"
|
|
15
16
|
},
|
|
16
17
|
"private": true,
|
|
17
18
|
"license": "MIT",
|
|
@@ -21,6 +21,18 @@ import { existsSync } from 'node:fs';
|
|
|
21
21
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
22
22
|
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
23
23
|
|
|
24
|
+
// Direct file import (not the bare package specifier) so the build.mjs
|
|
25
|
+
// import-rewriter produces a full-filename relative path — Node ESM won't
|
|
26
|
+
// resolve a directory-only path without a package.json main entry, and
|
|
27
|
+
// the bundled dist tree does not carry one for internal packages.
|
|
28
|
+
import {
|
|
29
|
+
BANNER_JS_TAG,
|
|
30
|
+
GLOBAL_BANNER_HTML,
|
|
31
|
+
THEME_CSS_LINK,
|
|
32
|
+
THEME_JS_TAG,
|
|
33
|
+
prepaintScriptTag,
|
|
34
|
+
} from '../../../engineering-pa-workspace/index.mjs';
|
|
35
|
+
|
|
24
36
|
/** Kebab-case validator: 2-40 chars, lowercase letters + digits + hyphen, starts with a letter. */
|
|
25
37
|
const NAME_RE = /^[a-z][a-z0-9-]{1,39}$/;
|
|
26
38
|
|
|
@@ -163,21 +175,41 @@ export async function mergeNavEntry({ navPath, name, label, section }) {
|
|
|
163
175
|
|
|
164
176
|
/**
|
|
165
177
|
* The static-page skeleton written for `<name>.html`. Kept minimal on
|
|
166
|
-
*
|
|
167
|
-
*
|
|
178
|
+
* content but complete on chrome: an extension page carries the same
|
|
179
|
+
* head + top-of-body contract every core page ships, so a freshly
|
|
180
|
+
* scaffolded page renders in the operator's chosen theme and shows the
|
|
181
|
+
* global status banner from the first load.
|
|
182
|
+
*
|
|
183
|
+
* The head tags come from
|
|
184
|
+
* `@stravica-ai/engineering-pa-workspace/src/workspace-page-contract.mjs`
|
|
185
|
+
* so a scaffolded page and a core page can never drift on the theme
|
|
186
|
+
* storage key or the tag order. Previously this used a single
|
|
187
|
+
* `<link rel="stylesheet" href="/core/tools/workspace/app/shell.css" />`
|
|
188
|
+
* — that path never shipped, so every scaffolded page rendered
|
|
189
|
+
* unstyled and outside the theme system.
|
|
190
|
+
*
|
|
191
|
+
* Core pages still hand-copy the contract for now (their inline `<style>`
|
|
192
|
+
* blocks sit between the theme tags and `banner.js`, so a full migration
|
|
193
|
+
* is more than a mechanical substitution and is tracked as a follow-up).
|
|
194
|
+
* A parity test pins the tokens so a rename in either place fails fast.
|
|
168
195
|
*/
|
|
169
196
|
export function buildHtmlSkeleton({ name, label }) {
|
|
197
|
+
const title = escapeHtml(label);
|
|
170
198
|
return `<!doctype html>
|
|
171
199
|
<html lang="en">
|
|
172
200
|
<head>
|
|
173
201
|
<meta charset="utf-8" />
|
|
174
202
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
175
|
-
<title>${
|
|
176
|
-
|
|
203
|
+
<title>${title}</title>
|
|
204
|
+
${prepaintScriptTag()}
|
|
205
|
+
${THEME_CSS_LINK}
|
|
206
|
+
${THEME_JS_TAG}
|
|
207
|
+
${BANNER_JS_TAG}
|
|
177
208
|
</head>
|
|
178
209
|
<body>
|
|
210
|
+
${GLOBAL_BANNER_HTML}
|
|
179
211
|
<main class="page">
|
|
180
|
-
<h1>${
|
|
212
|
+
<h1>${title}</h1>
|
|
181
213
|
<!-- workspace-extension:${name} -->
|
|
182
214
|
<p>Replace this with the real page. Data lives under <code>/__api/${name}/...</code> if you scaffolded the api stub.</p>
|
|
183
215
|
</main>
|
|
@@ -112,6 +112,12 @@ export async function runInit(options) {
|
|
|
112
112
|
*/
|
|
113
113
|
function writeNextStepMessage(config) {
|
|
114
114
|
const { paName, targetDir } = config;
|
|
115
|
+
// Prose renders the PA name Title-Cased so a sentence starting with it
|
|
116
|
+
// reads naturally (e.g. "Arry walks you through..." rather than
|
|
117
|
+
// "arry walks you through..."). The shell-alias lines still use the
|
|
118
|
+
// lowercase kebab-case identifier because that's what gets typed at a
|
|
119
|
+
// shell prompt.
|
|
120
|
+
const paTitle = paName.length ? paName[0].toUpperCase() + paName.slice(1) : paName;
|
|
115
121
|
const lines = [
|
|
116
122
|
'',
|
|
117
123
|
'Next step, start your PA:',
|
|
@@ -119,7 +125,7 @@ function writeNextStepMessage(config) {
|
|
|
119
125
|
` cd ${targetDir}`,
|
|
120
126
|
' pnpm exec pa session',
|
|
121
127
|
'',
|
|
122
|
-
`Inside the session, type \`/onboard\`. ${
|
|
128
|
+
`Inside the session, type \`/onboard\`. ${paTitle} walks you through the`,
|
|
123
129
|
'remaining verification and optional setup (runtime check, version check,',
|
|
124
130
|
'profile intake, alias, workspace, Atlassian). Every step is idempotent.',
|
|
125
131
|
'',
|
|
@@ -25,10 +25,54 @@ export function validatePaName(value) {
|
|
|
25
25
|
return { ok: true, value: trimmed };
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Strip one wrapping pair of straight or typographic quotes around a
|
|
30
|
+
* string, if present, then trim. Applied to name-class identity fields so
|
|
31
|
+
* an operator who types their name with wrapping quotes ("Barry" or
|
|
32
|
+
* U+201CBarryU+201D) never lands a quoted value in preferences.json — the
|
|
33
|
+
* greeting renderer is faithful, so a quoted store shows up on the home
|
|
34
|
+
* screen ("Evening, "Barry"").
|
|
35
|
+
*
|
|
36
|
+
* Handled pairs (any single pair around the whole value, whitespace
|
|
37
|
+
* tolerated on the outside):
|
|
38
|
+
* - ASCII double quote U+0022
|
|
39
|
+
* - ASCII single quote U+0027
|
|
40
|
+
* - Left/right double quote U+201C U+201D
|
|
41
|
+
* - Left/right single quote U+2018 U+2019
|
|
42
|
+
* - Left/right guillemet U+00AB U+00BB
|
|
43
|
+
*
|
|
44
|
+
* Only ONE wrapping pair is stripped; a value that is deliberately quoted
|
|
45
|
+
* for another reason (e.g. `'''`) still surfaces as data. Whitespace
|
|
46
|
+
* inside the quotes is preserved by the trim after the strip.
|
|
47
|
+
*
|
|
48
|
+
* @param {string} value
|
|
49
|
+
* @returns {string}
|
|
50
|
+
*/
|
|
51
|
+
export function stripWrappingQuotes(value) {
|
|
52
|
+
if (typeof value !== 'string') return value;
|
|
53
|
+
let s = value.trim();
|
|
54
|
+
const pairs = [
|
|
55
|
+
['"', '"'],
|
|
56
|
+
["'", "'"],
|
|
57
|
+
['“', '”'],
|
|
58
|
+
['‘', '’'],
|
|
59
|
+
['«', '»'],
|
|
60
|
+
];
|
|
61
|
+
for (const [open, close] of pairs) {
|
|
62
|
+
if (s.length >= 2 && s.startsWith(open) && s.endsWith(close)) {
|
|
63
|
+
s = s.slice(open.length, s.length - close.length).trim();
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return s;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Non-empty, 1-80 chars, trimmed, wrapping quotes stripped. Allows letters / spaces / punctuation. */
|
|
29
71
|
export function validateOperatorName(value) {
|
|
30
72
|
if (typeof value !== 'string') return { ok: false, reason: 'must be a string' };
|
|
31
|
-
|
|
73
|
+
// Strip wrapping quotes BEFORE the length check so a padded typographic
|
|
74
|
+
// pair around a short name never trips the max-length ceiling.
|
|
75
|
+
const trimmed = stripWrappingQuotes(value);
|
|
32
76
|
if (trimmed.length < 1) return { ok: false, reason: 'must not be empty' };
|
|
33
77
|
if (trimmed.length > 80) return { ok: false, reason: 'must be at most 80 characters' };
|
|
34
78
|
return { ok: true, value: trimmed };
|
|
@@ -64,7 +108,7 @@ export async function promptValues(seam, defaults) {
|
|
|
64
108
|
out.operatorName = await ensureValid(
|
|
65
109
|
seam,
|
|
66
110
|
defaults.operatorName,
|
|
67
|
-
|
|
111
|
+
'Operator name (the human this PA serves, e.g. Baz): ',
|
|
68
112
|
validateOperatorName,
|
|
69
113
|
);
|
|
70
114
|
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Two themes live here: dark (default) and light, toggled by the operator
|
|
5
5
|
* via the banner button. Selection is persisted in localStorage under
|
|
6
|
-
* `
|
|
7
|
-
*
|
|
6
|
+
* `dop.theme` (product-generic; legacy `dave.theme` is read once and
|
|
7
|
+
* migrated forward) and applied to `documentElement.dataset.theme` before
|
|
8
|
+
* first paint by the inline init script in each page's <head>.
|
|
8
9
|
*
|
|
9
10
|
* Token set is the union of every page's prior `:root` block. Both themes
|
|
10
11
|
* MUST define the same set of names — any unilateral token becomes invisible
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Two responsibilities split across two surfaces:
|
|
5
5
|
*
|
|
6
6
|
* 1. Pre-paint init. Each page also embeds a tiny inline <script> in <head>
|
|
7
|
-
* that reads `localStorage.
|
|
7
|
+
* that reads `localStorage.dop.theme` and sets
|
|
8
8
|
* `documentElement.dataset.theme` BEFORE first paint. That avoids a
|
|
9
9
|
* light-flash on a dark-preference user (FOUC). The inline script is the
|
|
10
10
|
* duplicate of `applyStoredTheme()` below; keeping it inline is the
|
|
@@ -18,7 +18,17 @@
|
|
|
18
18
|
* context (matching banner.js's behaviour) so a page opened directly
|
|
19
19
|
* still owns its own toggle.
|
|
20
20
|
*
|
|
21
|
-
* Storage: localStorage key `
|
|
21
|
+
* Storage: localStorage key `dop.theme`, values 'dark' | 'light'. The
|
|
22
|
+
* canonical token lives in `src/workspace-page-contract.mjs`; this file
|
|
23
|
+
* hand-copies the literal because it runs in the browser and cannot
|
|
24
|
+
* import ESM modules from that path. The theme parity test pins the two
|
|
25
|
+
* copies to the same value.
|
|
26
|
+
*
|
|
27
|
+
* Legacy: `dave.theme` is the pre-rename key. On first boot after the
|
|
28
|
+
* rename we read it once, copy the value to `dop.theme`, and never touch
|
|
29
|
+
* it again — nobody re-picks their theme just because the estate went
|
|
30
|
+
* product-generic. See `src/workspace-page-contract.mjs`.
|
|
31
|
+
*
|
|
22
32
|
* Default when unset: 'dark' (operator preference — see CLAUDE.md). We do
|
|
23
33
|
* NOT consult prefers-color-scheme; dark is the chosen default and we don't
|
|
24
34
|
* want auto-detection overriding it on first load.
|
|
@@ -26,15 +36,22 @@
|
|
|
26
36
|
(function () {
|
|
27
37
|
'use strict';
|
|
28
38
|
|
|
29
|
-
var STORAGE_KEY = '
|
|
39
|
+
var STORAGE_KEY = 'dop.theme';
|
|
40
|
+
var LEGACY_STORAGE_KEY = 'dave.theme';
|
|
30
41
|
var DEFAULT_THEME = 'dark';
|
|
31
42
|
var VALID = { dark: 1, light: 1 };
|
|
32
|
-
var MESSAGE_TYPE = '
|
|
43
|
+
var MESSAGE_TYPE = 'dop.theme.set';
|
|
33
44
|
|
|
34
45
|
function readStored() {
|
|
35
46
|
try {
|
|
36
47
|
var raw = localStorage.getItem(STORAGE_KEY);
|
|
37
48
|
if (raw && VALID[raw]) return raw;
|
|
49
|
+
// Legacy fallback: one-time migration from the pre-rename key.
|
|
50
|
+
var legacy = localStorage.getItem(LEGACY_STORAGE_KEY);
|
|
51
|
+
if (legacy && VALID[legacy]) {
|
|
52
|
+
try { localStorage.setItem(STORAGE_KEY, legacy); } catch (_e) { /* no-op */ }
|
|
53
|
+
return legacy;
|
|
54
|
+
}
|
|
38
55
|
} catch (_e) { /* storage unavailable — fall through to default */ }
|
|
39
56
|
return DEFAULT_THEME;
|
|
40
57
|
}
|
|
@@ -148,12 +165,18 @@
|
|
|
148
165
|
boot();
|
|
149
166
|
}
|
|
150
167
|
|
|
151
|
-
// Expose for tests + the inline init helper.
|
|
152
|
-
|
|
168
|
+
// Expose for tests + the inline init helper. `__daveTheme` is kept as an
|
|
169
|
+
// alias for anything under `operator/extensions/**` that grew a direct
|
|
170
|
+
// reference before the rename; `__dopTheme` is the canonical name and
|
|
171
|
+
// is what new callers should use.
|
|
172
|
+
var api = {
|
|
153
173
|
read: readStored,
|
|
154
174
|
write: writeStored,
|
|
155
175
|
apply: applyTheme,
|
|
156
176
|
storageKey: STORAGE_KEY,
|
|
177
|
+
legacyStorageKey: LEGACY_STORAGE_KEY,
|
|
157
178
|
defaultTheme: DEFAULT_THEME,
|
|
158
179
|
};
|
|
180
|
+
window.__dopTheme = api;
|
|
181
|
+
window.__daveTheme = api;
|
|
159
182
|
})();
|
|
@@ -22,7 +22,11 @@
|
|
|
22
22
|
|
|
23
23
|
const ENDPOINT = '/__api/status-mon';
|
|
24
24
|
const POLL_INTERVAL_MS = 30_000;
|
|
25
|
-
|
|
25
|
+
// Product-generic key; the pre-rename store was 'dave.statusmon.dismissed'.
|
|
26
|
+
// Read-once fallback keeps existing dismissals across the rename so a
|
|
27
|
+
// resolved incident someone had already dismissed doesn't reappear.
|
|
28
|
+
const DISMISSED_KEY = 'dop.statusmon.dismissed';
|
|
29
|
+
const LEGACY_DISMISSED_KEY = 'dave.statusmon.dismissed';
|
|
26
30
|
|
|
27
31
|
const SLOT_ID = 'global-banner';
|
|
28
32
|
|
|
@@ -36,7 +40,15 @@
|
|
|
36
40
|
|
|
37
41
|
function readDismissed() {
|
|
38
42
|
try {
|
|
39
|
-
|
|
43
|
+
var raw = localStorage.getItem(DISMISSED_KEY);
|
|
44
|
+
if (!raw) {
|
|
45
|
+
// Legacy fallback: read the pre-rename store once and migrate.
|
|
46
|
+
var legacy = localStorage.getItem(LEGACY_DISMISSED_KEY);
|
|
47
|
+
if (legacy) {
|
|
48
|
+
try { localStorage.setItem(DISMISSED_KEY, legacy); } catch { /* no-op */ }
|
|
49
|
+
raw = legacy;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
40
52
|
if (!raw) return [];
|
|
41
53
|
const parsed = JSON.parse(raw);
|
|
42
54
|
if (!Array.isArray(parsed)) return [];
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Delegations — Workspace</title>
|
|
7
|
-
<script>(function(){try{var t=localStorage.getItem('dave.theme');document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
|
|
7
|
+
<script>(function(){try{var t=localStorage.getItem('dop.theme');if(!t){var legacy=localStorage.getItem('dave.theme');if(legacy==='light'||legacy==='dark'){t=legacy;try{localStorage.setItem('dop.theme',legacy);}catch(_){}}}document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
|
|
8
8
|
<link rel="stylesheet" href="/core/tools/workspace/app/assets/theme.css">
|
|
9
9
|
<script src="/core/tools/workspace/app/assets/theme.js" defer></script>
|
|
10
10
|
<style>
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Home</title>
|
|
7
|
-
<script>(function(){try{var t=localStorage.getItem('dave.theme');document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
|
|
7
|
+
<script>(function(){try{var t=localStorage.getItem('dop.theme');if(!t){var legacy=localStorage.getItem('dave.theme');if(legacy==='light'||legacy==='dark'){t=legacy;try{localStorage.setItem('dop.theme',legacy);}catch(_){}}}document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
|
|
8
8
|
<link rel="stylesheet" href="/core/tools/workspace/app/assets/theme.css">
|
|
9
9
|
<script src="/core/tools/workspace/app/assets/theme.js" defer></script>
|
|
10
10
|
<style>
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<title>Digital Operator</title>
|
|
7
7
|
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='3' fill='%236366f1'/%3E%3Ctext x='8' y='12' font-family='monospace' font-size='11' font-weight='bold' text-anchor='middle' fill='%23121218'%3ED%3C/text%3E%3C/svg%3E">
|
|
8
8
|
<script src="/banner.js" defer></script>
|
|
9
|
-
<script>(function(){try{var t=localStorage.getItem('dave.theme');document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
|
|
9
|
+
<script>(function(){try{var t=localStorage.getItem('dop.theme');if(!t){var legacy=localStorage.getItem('dave.theme');if(legacy==='light'||legacy==='dark'){t=legacy;try{localStorage.setItem('dop.theme',legacy);}catch(_){}}}document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
|
|
10
10
|
<link rel="stylesheet" href="/core/tools/workspace/app/assets/theme.css">
|
|
11
11
|
<script src="/core/tools/workspace/app/assets/theme.js" defer></script>
|
|
12
12
|
<style>
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Knowledge Map — Workspace</title>
|
|
7
|
-
<script>(function(){try{var t=localStorage.getItem('dave.theme');document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
|
|
7
|
+
<script>(function(){try{var t=localStorage.getItem('dop.theme');if(!t){var legacy=localStorage.getItem('dave.theme');if(legacy==='light'||legacy==='dark'){t=legacy;try{localStorage.setItem('dop.theme',legacy);}catch(_){}}}document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
|
|
8
8
|
<link rel="stylesheet" href="/core/tools/workspace/app/assets/theme.css">
|
|
9
9
|
<script src="/core/tools/workspace/app/assets/theme.js" defer></script>
|
|
10
10
|
<style>
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Knowledge</title>
|
|
7
|
-
<script>(function(){try{var t=localStorage.getItem('dave.theme');document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
|
|
7
|
+
<script>(function(){try{var t=localStorage.getItem('dop.theme');if(!t){var legacy=localStorage.getItem('dave.theme');if(legacy==='light'||legacy==='dark'){t=legacy;try{localStorage.setItem('dop.theme',legacy);}catch(_){}}}document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
|
|
8
8
|
<link rel="stylesheet" href="/core/tools/workspace/app/assets/theme.css">
|
|
9
9
|
<script src="/core/tools/workspace/app/assets/theme.js" defer></script>
|
|
10
10
|
<style>
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Notes — Dave</title>
|
|
7
|
-
<script>(function(){try{var t=localStorage.getItem('dave.theme');document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
|
|
7
|
+
<script>(function(){try{var t=localStorage.getItem('dop.theme');if(!t){var legacy=localStorage.getItem('dave.theme');if(legacy==='light'||legacy==='dark'){t=legacy;try{localStorage.setItem('dop.theme',legacy);}catch(_){}}}document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
|
|
8
8
|
<link rel="stylesheet" href="/core/tools/workspace/app/assets/theme.css">
|
|
9
9
|
<script src="/core/tools/workspace/app/assets/theme.js" defer></script>
|
|
10
10
|
<style>
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Welcome to your PA</title>
|
|
7
|
-
<script>(function(){try{var t=localStorage.getItem('dave.theme');document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
|
|
7
|
+
<script>(function(){try{var t=localStorage.getItem('dop.theme');if(!t){var legacy=localStorage.getItem('dave.theme');if(legacy==='light'||legacy==='dark'){t=legacy;try{localStorage.setItem('dop.theme',legacy);}catch(_){}}}document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
|
|
8
8
|
<link rel="stylesheet" href="/core/tools/workspace/app/assets/theme.css">
|
|
9
9
|
<script src="/core/tools/workspace/app/assets/theme.js" defer></script>
|
|
10
10
|
<style>
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Profile</title>
|
|
7
|
-
<script>(function(){try{var t=localStorage.getItem('dave.theme');document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
|
|
7
|
+
<script>(function(){try{var t=localStorage.getItem('dop.theme');if(!t){var legacy=localStorage.getItem('dave.theme');if(legacy==='light'||legacy==='dark'){t=legacy;try{localStorage.setItem('dop.theme',legacy);}catch(_){}}}document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
|
|
8
8
|
<link rel="stylesheet" href="/core/tools/workspace/app/assets/theme.css">
|
|
9
9
|
<script src="/core/tools/workspace/app/assets/theme.js" defer></script>
|
|
10
10
|
<style>
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Project Detail</title>
|
|
7
|
-
<script>(function(){try{var t=localStorage.getItem('dave.theme');document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
|
|
7
|
+
<script>(function(){try{var t=localStorage.getItem('dop.theme');if(!t){var legacy=localStorage.getItem('dave.theme');if(legacy==='light'||legacy==='dark'){t=legacy;try{localStorage.setItem('dop.theme',legacy);}catch(_){}}}document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
|
|
8
8
|
<link rel="stylesheet" href="/core/tools/workspace/app/assets/theme.css">
|
|
9
9
|
<script src="/core/tools/workspace/app/assets/theme.js" defer></script>
|
|
10
10
|
<style>
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Projects — Dave</title>
|
|
7
|
-
<script>(function(){try{var t=localStorage.getItem('dave.theme');document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
|
|
7
|
+
<script>(function(){try{var t=localStorage.getItem('dop.theme');if(!t){var legacy=localStorage.getItem('dave.theme');if(legacy==='light'||legacy==='dark'){t=legacy;try{localStorage.setItem('dop.theme',legacy);}catch(_){}}}document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
|
|
8
8
|
<link rel="stylesheet" href="/core/tools/workspace/app/assets/theme.css">
|
|
9
9
|
<script src="/core/tools/workspace/app/assets/theme.js" defer></script>
|
|
10
10
|
<style>
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Git Overview — Workspace</title>
|
|
7
|
-
<script>(function(){try{var t=localStorage.getItem('dave.theme');document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
|
|
7
|
+
<script>(function(){try{var t=localStorage.getItem('dop.theme');if(!t){var legacy=localStorage.getItem('dave.theme');if(legacy==='light'||legacy==='dark'){t=legacy;try{localStorage.setItem('dop.theme',legacy);}catch(_){}}}document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
|
|
8
8
|
<link rel="stylesheet" href="/core/tools/workspace/app/assets/theme.css">
|
|
9
9
|
<script src="/core/tools/workspace/app/assets/theme.js" defer></script>
|
|
10
10
|
<style>
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Schedules — Workspace</title>
|
|
7
|
-
<script>(function(){try{var t=localStorage.getItem('dave.theme');document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
|
|
7
|
+
<script>(function(){try{var t=localStorage.getItem('dop.theme');if(!t){var legacy=localStorage.getItem('dave.theme');if(legacy==='light'||legacy==='dark'){t=legacy;try{localStorage.setItem('dop.theme',legacy);}catch(_){}}}document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
|
|
8
8
|
<link rel="stylesheet" href="/core/tools/workspace/app/assets/theme.css">
|
|
9
9
|
<script src="/core/tools/workspace/app/assets/theme.js" defer></script>
|
|
10
10
|
<style>
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Secrets</title>
|
|
7
|
-
<script>(function(){try{var t=localStorage.getItem('dave.theme');document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
|
|
7
|
+
<script>(function(){try{var t=localStorage.getItem('dop.theme');if(!t){var legacy=localStorage.getItem('dave.theme');if(legacy==='light'||legacy==='dark'){t=legacy;try{localStorage.setItem('dop.theme',legacy);}catch(_){}}}document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
|
|
8
8
|
<link rel="stylesheet" href="/core/tools/workspace/app/assets/theme.css">
|
|
9
9
|
<script src="/core/tools/workspace/app/assets/theme.js" defer></script>
|
|
10
10
|
<style>
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Staging Inbox — Workspace</title>
|
|
7
|
-
<script>(function(){try{var t=localStorage.getItem('dave.theme');document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
|
|
7
|
+
<script>(function(){try{var t=localStorage.getItem('dop.theme');if(!t){var legacy=localStorage.getItem('dave.theme');if(legacy==='light'||legacy==='dark'){t=legacy;try{localStorage.setItem('dop.theme',legacy);}catch(_){}}}document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
|
|
8
8
|
<link rel="stylesheet" href="/core/tools/workspace/app/assets/theme.css">
|
|
9
9
|
<script src="/core/tools/workspace/app/assets/theme.js" defer></script>
|
|
10
10
|
<style>
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Tasks — Dave</title>
|
|
7
|
-
<script>(function(){try{var t=localStorage.getItem('dave.theme');document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
|
|
7
|
+
<script>(function(){try{var t=localStorage.getItem('dop.theme');if(!t){var legacy=localStorage.getItem('dave.theme');if(legacy==='light'||legacy==='dark'){t=legacy;try{localStorage.setItem('dop.theme',legacy);}catch(_){}}}document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
|
|
8
8
|
<link rel="stylesheet" href="/core/tools/workspace/app/assets/theme.css">
|
|
9
9
|
<script src="/core/tools/workspace/app/assets/theme.js" defer></script>
|
|
10
10
|
<style>
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Document Viewer</title>
|
|
7
|
-
<script>(function(){try{var t=localStorage.getItem('dave.theme');document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
|
|
7
|
+
<script>(function(){try{var t=localStorage.getItem('dop.theme');if(!t){var legacy=localStorage.getItem('dave.theme');if(legacy==='light'||legacy==='dark'){t=legacy;try{localStorage.setItem('dop.theme',legacy);}catch(_){}}}document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
|
|
8
8
|
<link rel="stylesheet" href="/core/tools/workspace/app/assets/theme.css">
|
|
9
9
|
<script src="/core/tools/workspace/app/assets/theme.js" defer></script>
|
|
10
10
|
<style>
|
|
@@ -10,3 +10,19 @@ export const version = '0.1.1';
|
|
|
10
10
|
export { loadNav } from './src/nav.mjs';
|
|
11
11
|
export { startPoller as startStatusMonPoller } from './src/status-mon/poller.mjs';
|
|
12
12
|
export { handleApi } from './src/api.mjs';
|
|
13
|
+
|
|
14
|
+
// Shared workspace-page contract — every core page and every scaffolded
|
|
15
|
+
// extension page carries the same head + top-of-body markup. Owning the
|
|
16
|
+
// tokens here stops the scaffold and the core pages drifting on the theme
|
|
17
|
+
// storage key.
|
|
18
|
+
export {
|
|
19
|
+
THEME_STORAGE_KEY,
|
|
20
|
+
THEME_LEGACY_STORAGE_KEY,
|
|
21
|
+
THEME_MESSAGE_TYPE,
|
|
22
|
+
THEME_CSS_LINK,
|
|
23
|
+
THEME_JS_TAG,
|
|
24
|
+
BANNER_JS_TAG,
|
|
25
|
+
GLOBAL_BANNER_HTML,
|
|
26
|
+
prepaintScriptTag,
|
|
27
|
+
standardHeadInner,
|
|
28
|
+
} from './src/workspace-page-contract.mjs';
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stravica-ai/engineering-pa-workspace",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "Workspace server + viewer — operator-facing local web UI for the PA platform.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.mjs",
|
|
7
7
|
"exports": {
|
|
8
|
-
".": "./index.mjs"
|
|
8
|
+
".": "./index.mjs",
|
|
9
|
+
"./index.mjs": "./index.mjs"
|
|
9
10
|
},
|
|
10
11
|
"dependencies": {
|
|
11
12
|
"@stravica-ai/engineering-pa-cli": "workspace:*",
|
|
@@ -46,6 +46,7 @@ import { homedir, platform } from 'node:os';
|
|
|
46
46
|
import { dirname, join, resolve } from 'node:path';
|
|
47
47
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
48
48
|
import { readPreferencesSync } from '../../engineering-pa-core/src/preferences.mjs';
|
|
49
|
+
import { findRepoRootSync } from './repo-root.mjs';
|
|
49
50
|
import {
|
|
50
51
|
darwinLogBasenames,
|
|
51
52
|
launchdLabelFor,
|
|
@@ -60,25 +61,13 @@ import {
|
|
|
60
61
|
const __filename = fileURLToPath(import.meta.url);
|
|
61
62
|
const __dirname = dirname(__filename);
|
|
62
63
|
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
// checkouts)
|
|
71
|
-
function findRepoRoot(start) {
|
|
72
|
-
let d = start;
|
|
73
|
-
while (d && d !== resolve(d, '..')) {
|
|
74
|
-
if (existsSync(join(d, '.git')) || existsSync(join(d, 'pnpm-workspace.yaml'))) return d;
|
|
75
|
-
d = resolve(d, '..');
|
|
76
|
-
}
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const envRoot = process.env.DAVE_REPO_ROOT || process.env.PA_REPO_ROOT;
|
|
81
|
-
const REPO_ROOT = envRoot ? resolve(envRoot) : (findRepoRoot(process.cwd()) || process.cwd());
|
|
64
|
+
// Instance-root resolution routed through the shared helper (see
|
|
65
|
+
// `./repo-root.mjs`). Under the published tarball layout, `__dirname`-relative
|
|
66
|
+
// walks land inside `node_modules/@stravica-ai/digital-operator-pa/dist/` and
|
|
67
|
+
// mis-target every derived path; the shared resolver walks up from cwd for
|
|
68
|
+
// `.git`, `operator/config/preferences.json`, or `pnpm-workspace.yaml`, and
|
|
69
|
+
// honours `DAVE_REPO_ROOT` / `PA_REPO_ROOT` overrides.
|
|
70
|
+
const REPO_ROOT = findRepoRootSync();
|
|
82
71
|
const SERVER_SCRIPT = join(__dirname, 'server.mjs');
|
|
83
72
|
|
|
84
73
|
/**
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// repo-root.mjs — shared sync resolver for the operator instance root.
|
|
2
|
+
//
|
|
3
|
+
// Under the published tarball layout, `@stravica-ai/engineering-pa-workspace`
|
|
4
|
+
// lives at `node_modules/@stravica-ai/digital-operator-pa/dist/engineering-pa-workspace/`.
|
|
5
|
+
// A `resolve(__dirname, '..', '..', '..')` walk from `src/` therefore lands
|
|
6
|
+
// inside the `digital-operator-pa` package directory (a pnpm-store hard-link),
|
|
7
|
+
// NOT at the operator repo root — silently misdirecting every file the
|
|
8
|
+
// workspace derives from that path (`.ws.pid`, `.ws.log`, preferences overlay
|
|
9
|
+
// reads, `paName` lookups, the server subprocess cwd).
|
|
10
|
+
//
|
|
11
|
+
// The resolver mirrors the precedence the rest of the workspace package
|
|
12
|
+
// already uses (`server.mjs`, `install-daemon.mjs`):
|
|
13
|
+
//
|
|
14
|
+
// 1. `DAVE_REPO_ROOT` or `PA_REPO_ROOT` env override (tests + operator-set).
|
|
15
|
+
// 2. Walk up from `process.cwd()` looking for a marker that identifies an
|
|
16
|
+
// operator instance root:
|
|
17
|
+
// - `.git` (present in every scaffolded instance — pa-init runs `git
|
|
18
|
+
// init` at scaffold time).
|
|
19
|
+
// - `operator/config/preferences.json` (the canonical instance marker;
|
|
20
|
+
// matches instances that lost their `.git` for any reason).
|
|
21
|
+
// - `pnpm-workspace.yaml` (matches monorepo checkouts, e.g. this
|
|
22
|
+
// engineering-pa-dev repo itself, so package-local tests still work).
|
|
23
|
+
// 3. Fall back to `process.cwd()` so pre-scaffold and dry-run flows still
|
|
24
|
+
// have a stable value to render.
|
|
25
|
+
//
|
|
26
|
+
// The `startDir` argument is injectable for tests and callers that need to
|
|
27
|
+
// resolve from a directory other than `process.cwd()`. Prod callers use
|
|
28
|
+
// the default.
|
|
29
|
+
|
|
30
|
+
import { existsSync } from 'node:fs';
|
|
31
|
+
import { join, resolve } from 'node:path';
|
|
32
|
+
|
|
33
|
+
const MARKERS = [
|
|
34
|
+
'.git',
|
|
35
|
+
join('operator', 'config', 'preferences.json'),
|
|
36
|
+
'pnpm-workspace.yaml',
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Walk up from `start` looking for the first directory that contains any
|
|
41
|
+
* marker in {@link MARKERS}. Returns the directory path, or null when the
|
|
42
|
+
* walk reaches the filesystem root without a hit.
|
|
43
|
+
*
|
|
44
|
+
* @param {string} start
|
|
45
|
+
* @returns {string|null}
|
|
46
|
+
*/
|
|
47
|
+
export function walkUpForMarker(start) {
|
|
48
|
+
let d = resolve(start);
|
|
49
|
+
while (d && d !== resolve(d, '..')) {
|
|
50
|
+
for (const marker of MARKERS) {
|
|
51
|
+
if (existsSync(join(d, marker))) return d;
|
|
52
|
+
}
|
|
53
|
+
d = resolve(d, '..');
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Process-level guard so the fallback warning fires at most once per
|
|
59
|
+
// process, no matter how many callers hit the resolver. Tests can reset
|
|
60
|
+
// via the exported `_resetFallbackWarningForTests` helper below.
|
|
61
|
+
let _fallbackWarned = false;
|
|
62
|
+
|
|
63
|
+
/** Test-only reset for the once-per-process fallback warning latch. */
|
|
64
|
+
export function _resetFallbackWarningForTests() {
|
|
65
|
+
_fallbackWarned = false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Resolve the operator instance root. Deterministic and synchronous; safe
|
|
70
|
+
* to call at module top-level. See the header comment for the full
|
|
71
|
+
* precedence rule.
|
|
72
|
+
*
|
|
73
|
+
* When no marker is found AND no env override is set, emit ONE stderr
|
|
74
|
+
* warning naming the cwd we're falling back to and the two overrides that
|
|
75
|
+
* would silence it. The walk still returns cwd so pre-scaffold and dry-run
|
|
76
|
+
* flows keep working — the warning is a diagnostic, not an error.
|
|
77
|
+
*
|
|
78
|
+
* @param {object} [opts]
|
|
79
|
+
* @param {string} [opts.startDir=process.cwd()] Directory to start the walk from.
|
|
80
|
+
* @param {NodeJS.ProcessEnv} [opts.env=process.env] Env source (tests inject).
|
|
81
|
+
* @param {(msg: string) => void} [opts.warn] Optional warn sink (tests inject).
|
|
82
|
+
* @returns {string} Absolute path.
|
|
83
|
+
*/
|
|
84
|
+
export function findRepoRootSync(opts = {}) {
|
|
85
|
+
const env = opts.env || process.env;
|
|
86
|
+
const startDir = opts.startDir || process.cwd();
|
|
87
|
+
const envRoot = env.DAVE_REPO_ROOT || env.PA_REPO_ROOT;
|
|
88
|
+
if (envRoot) return resolve(envRoot);
|
|
89
|
+
const found = walkUpForMarker(startDir);
|
|
90
|
+
if (found) return found;
|
|
91
|
+
const fallback = resolve(startDir);
|
|
92
|
+
if (!_fallbackWarned) {
|
|
93
|
+
_fallbackWarned = true;
|
|
94
|
+
const warn = opts.warn || ((m) => process.stderr.write(m + '\n'));
|
|
95
|
+
warn(
|
|
96
|
+
`workspace: no instance marker (.git, operator/config/preferences.json, pnpm-workspace.yaml) found walking up from ${fallback}; falling back to cwd. Set DAVE_REPO_ROOT or PA_REPO_ROOT to silence.`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
return fallback;
|
|
100
|
+
}
|
|
@@ -33,32 +33,18 @@ import { buildAllowedHosts, buildAllowedOrigins, checkRequestGuards, isGuardedPa
|
|
|
33
33
|
import { readTeamScopeConfigSync } from '../../engineering-pa-core/src/desk/team-scope.mjs';
|
|
34
34
|
import { readPreferencesSync } from '../../engineering-pa-core/src/preferences.mjs';
|
|
35
35
|
import { startPoller as startStatusMonPoller } from './status-mon/poller.mjs';
|
|
36
|
+
import { findRepoRootSync } from './repo-root.mjs';
|
|
36
37
|
|
|
37
38
|
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
38
39
|
|
|
39
40
|
// In v2 the workspace package is installed inside the operator repo at
|
|
40
|
-
// `node_modules/@stravica-ai/engineering-pa-workspace/`. The operator repo
|
|
41
|
-
// is
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
while (d && d !== prev) {
|
|
48
|
-
try {
|
|
49
|
-
const s = existsSync(join(d, '.git'));
|
|
50
|
-
if (s) return d;
|
|
51
|
-
} catch { /* ignore */ }
|
|
52
|
-
prev = d;
|
|
53
|
-
d = resolve(d, '..');
|
|
54
|
-
}
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const envRepoRoot = process.env.DAVE_REPO_ROOT || process.env.PA_REPO_ROOT;
|
|
59
|
-
const REPO_ROOT = envRepoRoot
|
|
60
|
-
? resolve(envRepoRoot)
|
|
61
|
-
: (findRepoRootSync(process.cwd()) || process.cwd());
|
|
41
|
+
// `node_modules/@stravica-ai/engineering-pa-workspace/`. The operator repo
|
|
42
|
+
// root is resolved via the shared helper (`./repo-root.mjs`), which walks
|
|
43
|
+
// up from cwd for `.git`, `operator/config/preferences.json`, or
|
|
44
|
+
// `pnpm-workspace.yaml`, and honours `DAVE_REPO_ROOT` / `PA_REPO_ROOT`
|
|
45
|
+
// overrides. The package's own assets live at `../app/` (one level up
|
|
46
|
+
// from src/).
|
|
47
|
+
const REPO_ROOT = findRepoRootSync();
|
|
62
48
|
const APP_DIR = resolve(__dirname, '..', 'app');
|
|
63
49
|
|
|
64
50
|
// Silent by default. Set WS_DEBUG=1 to re-enable startup banner, file-change
|
|
@@ -232,8 +218,9 @@ function resetIdle() {
|
|
|
232
218
|
// palette block, which broke the theme toggle on the content-calendar
|
|
233
219
|
// preview iframe (operator-reported, 2026-06-27). The fix matches what
|
|
234
220
|
// viewer.html does: link the shared theme.css, defer-load theme.js, pre-paint
|
|
235
|
-
// init reads localStorage '
|
|
236
|
-
//
|
|
221
|
+
// init reads localStorage 'dop.theme' (with a one-time legacy 'dave.theme'
|
|
222
|
+
// fallback), body in humanist sans for prose, and .md-body strong uses the
|
|
223
|
+
// new --emphasis token (matching .content strong).
|
|
237
224
|
// Exported so unit tests can assert on the output without spinning up the
|
|
238
225
|
// HTTP server.
|
|
239
226
|
export function markdownPage(rawContent, filePath) {
|
|
@@ -248,7 +235,7 @@ export function markdownPage(rawContent, filePath) {
|
|
|
248
235
|
<meta charset="UTF-8">
|
|
249
236
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
250
237
|
<title>${title}</title>
|
|
251
|
-
<script>(function(){try{var t=localStorage.getItem('dave.theme');document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
|
|
238
|
+
<script>(function(){try{var t=localStorage.getItem('dop.theme');if(!t){var legacy=localStorage.getItem('dave.theme');if(legacy==='light'||legacy==='dark'){t=legacy;try{localStorage.setItem('dop.theme',legacy);}catch(_){}}}document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>
|
|
252
239
|
<link rel="stylesheet" href="/core/tools/workspace/app/assets/theme.css">
|
|
253
240
|
<script src="/core/tools/workspace/app/assets/theme.js" defer></script>
|
|
254
241
|
<style>
|
|
@@ -29,6 +29,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
29
29
|
|
|
30
30
|
import { readPreferencesSync } from '../../../engineering-pa-core/src/preferences.mjs';
|
|
31
31
|
import { poll, resolveSources, mergeWithPrevious } from './poll.mjs';
|
|
32
|
+
import { findRepoRootSync } from '../repo-root.mjs';
|
|
32
33
|
|
|
33
34
|
const __filename = fileURLToPath(import.meta.url);
|
|
34
35
|
const __dirname = dirname(__filename);
|
|
@@ -61,15 +62,15 @@ function parseArgs(argv) {
|
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
function findRepoRoot() {
|
|
65
|
+
// DAVE_ROOT is the historical status-mon env; keep it as a first-class
|
|
66
|
+
// override so scheduled runs already setting it are unaffected. Otherwise
|
|
67
|
+
// route through the shared resolver (`../repo-root.mjs`), which walks up
|
|
68
|
+
// from cwd for the instance markers and also honours DAVE_REPO_ROOT /
|
|
69
|
+
// PA_REPO_ROOT. The old fallback (`resolve(__dirname, '..', '..', '..')`)
|
|
70
|
+
// was broken under the installed tarball layout — see
|
|
71
|
+
// `w-2026-09-17-dave-016`.
|
|
64
72
|
if (process.env.DAVE_ROOT) return process.env.DAVE_ROOT;
|
|
65
|
-
|
|
66
|
-
while (dir !== '/' && dir !== '.') {
|
|
67
|
-
if (existsSync(join(dir, '.git'))) return dir;
|
|
68
|
-
const parent = dirname(dir);
|
|
69
|
-
if (parent === dir) break;
|
|
70
|
-
dir = parent;
|
|
71
|
-
}
|
|
72
|
-
return resolve(__dirname, '..', '..', '..');
|
|
73
|
+
return findRepoRootSync();
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
function readPreviousState(statePath) {
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// workspace-page-contract.mjs — the single source of truth for the head
|
|
2
|
+
// and top-of-body markup every workspace HTML page must ship.
|
|
3
|
+
//
|
|
4
|
+
// Every page served by the workspace (core pages under `packages/workspace/app/`
|
|
5
|
+
// and extension pages scaffolded by `pa-init workspace-extension`) has to
|
|
6
|
+
// carry the same four head tags in the same order:
|
|
7
|
+
//
|
|
8
|
+
// 1. Inline pre-paint script that reads the theme from localStorage and
|
|
9
|
+
// sets `documentElement.dataset.theme` BEFORE first paint. Without
|
|
10
|
+
// this, dark-preference users see a light-flash (FOUC).
|
|
11
|
+
// 2. Link to `/core/tools/workspace/app/assets/theme.css` (the shared
|
|
12
|
+
// palette + `[data-theme=...]` token blocks).
|
|
13
|
+
// 3. Deferred `/core/tools/workspace/app/assets/theme.js` (toggle button,
|
|
14
|
+
// cross-frame coordination, storage sync).
|
|
15
|
+
// 4. Deferred `/banner.js` (the global status banner poller).
|
|
16
|
+
//
|
|
17
|
+
// And the body's first child must be `<div id="global-banner"></div>` so
|
|
18
|
+
// `banner.js` has a mount point.
|
|
19
|
+
//
|
|
20
|
+
// This module owns the string forms of all four so a scaffold-generated
|
|
21
|
+
// extension page and a hand-authored core page can never drift. Core pages
|
|
22
|
+
// still hand-copy the block for now (their inline `<style>` sits after
|
|
23
|
+
// theme.js in the head, so a full migration is more than a mechanical
|
|
24
|
+
// substitution and is left as a follow-up); the scaffold and the theme
|
|
25
|
+
// parity test import from here.
|
|
26
|
+
//
|
|
27
|
+
// Storage-key policy: the theme key is `'dop.theme'` (product-generic —
|
|
28
|
+
// `dop` = Digital Operator PA). Legacy stores under `'dave.theme'` are
|
|
29
|
+
// migrated on first read (see `theme.js`) so nobody re-picks their theme
|
|
30
|
+
// after the rename.
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* localStorage key the theme selection persists under. Product-generic;
|
|
34
|
+
* shared by the pre-paint bootstrap, theme.js, and every consuming page.
|
|
35
|
+
*/
|
|
36
|
+
export const THEME_STORAGE_KEY = 'dop.theme';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Legacy localStorage key. Read once on first boot when the new key is
|
|
40
|
+
* absent and copied forward. Never written. Kept exported so tests can
|
|
41
|
+
* pin the migration path without hardcoding the literal.
|
|
42
|
+
*/
|
|
43
|
+
export const THEME_LEGACY_STORAGE_KEY = 'dave.theme';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* postMessage type the shell uses to broadcast a theme change into
|
|
47
|
+
* same-origin iframed pages. Product-generic; renamed alongside the
|
|
48
|
+
* storage key.
|
|
49
|
+
*/
|
|
50
|
+
export const THEME_MESSAGE_TYPE = 'dop.theme.set';
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The inline pre-paint bootstrap. Reads the new key, falls back to the
|
|
54
|
+
* legacy key when the new one is unset, applies to
|
|
55
|
+
* `documentElement.dataset.theme` before first paint. Same shape the
|
|
56
|
+
* markdownPage renderer emits so a fresh direct-load never flashes.
|
|
57
|
+
*
|
|
58
|
+
* Returned as a self-contained `<script>...</script>` tag; embed it in
|
|
59
|
+
* `<head>` BEFORE the theme.css link and before any body content.
|
|
60
|
+
*/
|
|
61
|
+
export function prepaintScriptTag() {
|
|
62
|
+
return `<script>(function(){try{var t=localStorage.getItem('${THEME_STORAGE_KEY}');if(!t){var legacy=localStorage.getItem('${THEME_LEGACY_STORAGE_KEY}');if(legacy==='light'||legacy==='dark'){t=legacy;try{localStorage.setItem('${THEME_STORAGE_KEY}',legacy);}catch(_){}}}document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(_){document.documentElement.dataset.theme='dark';}})();</script>`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Link tag for the shared theme stylesheet. */
|
|
66
|
+
export const THEME_CSS_LINK = '<link rel="stylesheet" href="/core/tools/workspace/app/assets/theme.css">';
|
|
67
|
+
|
|
68
|
+
/** Deferred script tag for the shared theme.js toggle + storage coordinator. */
|
|
69
|
+
export const THEME_JS_TAG = '<script src="/core/tools/workspace/app/assets/theme.js" defer></script>';
|
|
70
|
+
|
|
71
|
+
/** Deferred script tag for the global status banner. */
|
|
72
|
+
export const BANNER_JS_TAG = '<script src="/banner.js" defer></script>';
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The mount point banner.js expects. Insert as the first child of `<body>`.
|
|
76
|
+
*/
|
|
77
|
+
export const GLOBAL_BANNER_HTML = '<div id="global-banner"></div>';
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Build the standard `<head>` inner markup a workspace page must ship.
|
|
81
|
+
* Charset, viewport, `<title>`, then the four theme + banner tags in the
|
|
82
|
+
* documented order. Callers append their own `<style>` block (if any) AFTER
|
|
83
|
+
* the returned string and BEFORE closing `</head>`.
|
|
84
|
+
*
|
|
85
|
+
* @param {object} opts
|
|
86
|
+
* @param {string} opts.title page title (already HTML-escaped by caller)
|
|
87
|
+
*/
|
|
88
|
+
export function standardHeadInner({ title }) {
|
|
89
|
+
return [
|
|
90
|
+
'<meta charset="utf-8" />',
|
|
91
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1" />',
|
|
92
|
+
`<title>${title}</title>`,
|
|
93
|
+
prepaintScriptTag(),
|
|
94
|
+
THEME_CSS_LINK,
|
|
95
|
+
THEME_JS_TAG,
|
|
96
|
+
BANNER_JS_TAG,
|
|
97
|
+
].join('\n');
|
|
98
|
+
}
|
|
@@ -28,10 +28,20 @@ import {
|
|
|
28
28
|
restartCommand,
|
|
29
29
|
} from './supervisor.mjs';
|
|
30
30
|
import { resolvePaName } from './unit-name.mjs';
|
|
31
|
+
import { findRepoRootSync } from './repo-root.mjs';
|
|
31
32
|
|
|
32
33
|
const __filename = fileURLToPath(import.meta.url);
|
|
33
34
|
const __dirname = dirname(__filename);
|
|
34
|
-
|
|
35
|
+
// Instance root — NOT `resolve(__dirname, '..', '..', '..')`. Under the
|
|
36
|
+
// published `@stravica-ai/digital-operator-pa` tarball layout the workspace
|
|
37
|
+
// package lives under `dist/engineering-pa-workspace/`, so a three-parent
|
|
38
|
+
// __dirname walk lands inside the pnpm-store hard-linked package directory
|
|
39
|
+
// rather than at the operator instance root. `findRepoRootSync` walks up
|
|
40
|
+
// from `process.cwd()` for `.git` / `operator/config/preferences.json` /
|
|
41
|
+
// `pnpm-workspace.yaml` and honours `DAVE_REPO_ROOT` / `PA_REPO_ROOT`
|
|
42
|
+
// overrides. See `./repo-root.mjs` for the full precedence rule and
|
|
43
|
+
// `w-2026-09-17-dave-016` for the P1 that caused this to be extracted.
|
|
44
|
+
const REPO_ROOT = findRepoRootSync();
|
|
35
45
|
|
|
36
46
|
const SERVER_SCRIPT = join(__dirname, 'server.mjs');
|
|
37
47
|
// Env overrides let tests point at a temp directory so the real repo's
|
|
@@ -326,6 +336,7 @@ async function cmdStart() {
|
|
|
326
336
|
const who = holder ? ` (held by PID ${holder.pid}${holder.command ? ` — ${holder.command}` : ''})` : '';
|
|
327
337
|
console.error(`Error: Stale PID file references dead PID ${existing.pid}, but port ${existing.port} is in use${who}.`);
|
|
328
338
|
console.error('Refusing to kill an unrelated process. Remove the PID file manually or use a different port.');
|
|
339
|
+
console.error('Hint: run `pa workspace start --port <N>` with a free port, or set `workspacePort` in operator/config/preferences.json.');
|
|
329
340
|
process.exit(1);
|
|
330
341
|
}
|
|
331
342
|
removePid();
|
|
@@ -334,6 +345,7 @@ async function cmdStart() {
|
|
|
334
345
|
// Check if port is already in use by something else
|
|
335
346
|
if (await checkPort(port)) {
|
|
336
347
|
console.error(`Error: Port ${port} is already in use by another process.`);
|
|
348
|
+
console.error('Hint: run `pa workspace start --port <N>` with a free port, or set `workspacePort` in operator/config/preferences.json.');
|
|
337
349
|
process.exit(1);
|
|
338
350
|
}
|
|
339
351
|
|
package/docs/what-it-can-do.md
CHANGED
|
@@ -43,7 +43,7 @@ The PA is a Digital Operator that runs inside Claude Code on your laptop: it run
|
|
|
43
43
|
|
|
44
44
|
- The dispatch-briefer analyses your request and writes an eight-section brief in fresh context, covering boundaries, outputs and completion criteria.
|
|
45
45
|
- Work can go to a single agent or a task group with a coordinator and workers, or stay in the current conversation.
|
|
46
|
-
- Parallel workers use separate git worktrees in the
|
|
46
|
+
- Parallel workers use separate git worktrees in the operator's own repositories; writes to downstream project repositories run sequentially.
|
|
47
47
|
- A coordinator hook enforces tool-use policy inside Claude Code.
|
|
48
48
|
|
|
49
49
|
## The roles it comes with
|
|
@@ -74,12 +74,12 @@ The PA is a Digital Operator that runs inside Claude Code on your laptop: it run
|
|
|
74
74
|
|
|
75
75
|
- Solo use is the shipped setup; team lead and member modes are not implemented.
|
|
76
76
|
- The remote library is dormant in the default local mode.
|
|
77
|
-
-
|
|
77
|
+
- Specialised roles and the session-boot context injections used on the first-party estate are not part of the shipped package.
|
|
78
78
|
- Connections to services through MCP need your own Claude Code configuration.
|
|
79
79
|
- You supply your voice guide, personal writing rules, principles and profile knowledge; the starter content is structure and placeholders.
|
|
80
80
|
- Sharing repositories or memory across machines needs a remote and coordination that you set up.
|
|
81
81
|
|
|
82
82
|
## How to start
|
|
83
83
|
|
|
84
|
-
Open
|
|
85
|
-
Follow it to set up
|
|
84
|
+
Open [docs/onboarding-solo.md](./onboarding-solo.md).
|
|
85
|
+
Follow it to set up your Digital Operator on your laptop.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stravica-ai/digital-operator-pa",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.48",
|
|
4
4
|
"description": "Digital Operator PA — single-bundled public package surface (bins + library) for the platform.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.mjs",
|
|
@@ -39,11 +39,8 @@
|
|
|
39
39
|
"LICENSE"
|
|
40
40
|
],
|
|
41
41
|
"license": "MIT",
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
"url": "git+https://github.com/Stravica/digital-operator-pa.git",
|
|
45
|
-
"directory": "packages/engineering-pa"
|
|
46
|
-
},
|
|
42
|
+
"homepage": "https://stravica.ai",
|
|
43
|
+
"bugs": "https://stravica.ai",
|
|
47
44
|
"publishConfig": {
|
|
48
45
|
"registry": "https://registry.npmjs.org/",
|
|
49
46
|
"access": "public"
|