@stravica-ai/digital-operator-pa 0.1.39 → 0.1.41
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/README.md +12 -2
- package/bin/confluence +9 -0
- package/bin/confluence-fetch +9 -0
- package/bin/jira +9 -0
- package/dist/engineering-pa-core/knowledge/content-voice/banned-ai-tells-taxonomy.md +1 -1
- package/dist/engineering-pa-core/knowledge/knowledge-memory/staging-and-observations.md +3 -3
- package/dist/engineering-pa-core/knowledge/platform-tooling/workspace-extensions.md +62 -0
- package/dist/engineering-pa-core-extensions-atlassian/README.md +81 -16
- package/dist/engineering-pa-core-extensions-atlassian/bin/confluence +2 -0
- package/dist/engineering-pa-core-extensions-atlassian/bin/confluence-fetch +2 -0
- package/dist/engineering-pa-core-extensions-atlassian/bin/jira +2 -0
- package/dist/engineering-pa-core-extensions-atlassian/config/jira-workflow-chains.default.json +4 -0
- package/dist/engineering-pa-core-extensions-atlassian/index.mjs +17 -6
- package/dist/engineering-pa-core-extensions-atlassian/package.json +11 -4
- package/dist/engineering-pa-core-extensions-atlassian/src/common/invalidate-client.mjs +137 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/common/pa-name.mjs +58 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/common/secrets.mjs +158 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/cli.mjs +165 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/client.mjs +294 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/auth.mjs +59 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/comment.mjs +174 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/create.mjs +136 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/label.mjs +98 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/mine.mjs +105 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/mirror-folder.mjs +487 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/mirror-page.mjs +351 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/page.mjs +127 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/pages.mjs +107 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/recent.mjs +107 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/search.mjs +123 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/spaces.mjs +82 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/test.mjs +155 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/tree.mjs +103 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/update.mjs +134 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/format.mjs +260 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/mirror.mjs +353 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/storage.mjs +880 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence-fetch/cli.mjs +313 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence-fetch/fetch.mjs +327 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/adf.mjs +388 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/bug-fields.mjs +294 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/cli.mjs +197 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/client.mjs +350 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/advance.mjs +486 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/attach.mjs +129 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/auth.mjs +53 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/boards.mjs +88 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/bulk-transition.mjs +223 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/cancel.mjs +135 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/comment.mjs +169 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/create.mjs +376 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/issue.mjs +105 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/link.mjs +204 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/mine.mjs +97 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/projects.mjs +91 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/search.mjs +80 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/sprint.mjs +111 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/sync-statuses.mjs +313 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/test.mjs +142 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/transition.mjs +197 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/update.mjs +284 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/worklog.mjs +380 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/format.mjs +267 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/invalidate.mjs +72 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/metadata.mjs +225 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/required-fields.mjs +334 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/workflow-chains.mjs +405 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/workflow-map.mjs +107 -0
- package/dist/engineering-pa-scaffold/README.md +32 -1
- package/dist/engineering-pa-scaffold/src/cli.mjs +83 -6
- package/dist/engineering-pa-scaffold/src/commands/init-workspace-extension.mjs +234 -0
- package/dist/engineering-pa-scaffold/src/commands/init.mjs +6 -5
- package/dist/engineering-pa-scaffold/src/generator.mjs +240 -4
- package/dist/engineering-pa-scaffold/templates/CLAUDE.md.template +7 -7
- package/dist/engineering-pa-scaffold/templates/operator-README.md.template +4 -4
- package/dist/engineering-pa-scaffold/templates/root-CLAUDE.md.template +13 -0
- package/dist/engineering-pa-types-engineering/index.mjs +14 -6
- package/dist/engineering-pa-types-engineering/knowledge/rcf-lite/create-a-small-app.md +81 -0
- package/dist/engineering-pa-types-engineering/knowledge/rcf-lite/for-agents.md +25 -0
- package/dist/engineering-pa-types-engineering/knowledge/rcf-lite/index.md +34 -0
- package/dist/engineering-pa-types-engineering/knowledge/rcf-lite/reference.md +16 -0
- package/dist/engineering-pa-types-engineering/knowledge/rcf-lite/wire-your-agent.md +17 -0
- package/dist/engineering-pa-types-engineering/package.json +2 -2
- package/dist/engineering-pa-types-engineering/roles/rcf-lite-engineer/definition.md +183 -0
- package/dist/engineering-pa-workspace/app/home.html +7 -7
- package/dist/engineering-pa-workspace/app/onboarding.html +4 -4
- package/dist/engineering-pa-workspace/src/api.mjs +1 -1
- package/dist/engineering-pa-workspace/src/server.mjs +1 -1
- package/docs/installing.md +78 -0
- package/docs/onboarding-solo.md +162 -0
- package/docs/what-it-can-do.md +85 -0
- package/package.json +6 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// pa-init
|
|
1
|
+
// pa-init: onboarding scaffolder.
|
|
2
2
|
//
|
|
3
3
|
// Verb routing + arg parsing + exit-code translation. Domain code lives in
|
|
4
4
|
// `commands/`, prompts in `prompts.mjs`, file generation in `generator.mjs`,
|
|
@@ -14,25 +14,35 @@
|
|
|
14
14
|
import { parseArgs } from 'node:util';
|
|
15
15
|
|
|
16
16
|
import { runInit } from './commands/init.mjs';
|
|
17
|
+
import { runInitWorkspaceExtension } from './commands/init-workspace-extension.mjs';
|
|
17
18
|
|
|
18
|
-
const USAGE = `pa-init
|
|
19
|
+
const USAGE = `pa-init: scaffold a new personal-assistant operator repo, or a single workspace extension.
|
|
19
20
|
|
|
20
21
|
Usage:
|
|
21
22
|
pa-init --mode=solo [--name=PA_NAME] [--target=DIR] [--non-interactive] [--operator=NAME] [--purpose=TEXT]
|
|
23
|
+
pa-init workspace-extension <name> [--label=LABEL] [--section=SECTION] [--with-api] [--target=DIR]
|
|
22
24
|
pa-init --help
|
|
23
25
|
|
|
24
26
|
Modes:
|
|
25
27
|
solo Single-operator instance, no team library (Phase 1)
|
|
26
|
-
lead Team-lead instance with team library (Phase 5
|
|
27
|
-
member --invite=TOKEN Join an existing team library (Phase 5
|
|
28
|
+
lead Team-lead instance with team library (Phase 5, not yet implemented)
|
|
29
|
+
member --invite=TOKEN Join an existing team library (Phase 5, not yet implemented)
|
|
30
|
+
|
|
31
|
+
Sub-verbs:
|
|
32
|
+
workspace-extension Scaffold one workspace-extension page inside an existing operator repo.
|
|
33
|
+
Writes operator/extensions/workspace/<name>.html and appends nav.json.
|
|
34
|
+
--with-api also writes an <name>-api.mjs stub exporting register(addRoute).
|
|
28
35
|
|
|
29
36
|
Common flags:
|
|
30
37
|
--name=PA_NAME PA identity (kebab-case, lowercase, no spaces). Prompted if absent.
|
|
31
|
-
--target=DIR Target directory. Prompted if absent
|
|
38
|
+
--target=DIR Target directory. Prompted if absent for --mode=solo; defaults to cwd for sub-verbs.
|
|
32
39
|
--operator=NAME Operator (human) name. Prompted if absent.
|
|
33
40
|
--purpose=TEXT One-line PA purpose statement. Defaults to a generic line.
|
|
34
41
|
--non-interactive Refuse to prompt; require all values via flags. CI-friendly.
|
|
35
42
|
--skip-install Skip post-scaffold pnpm install + pa status (for testing).
|
|
43
|
+
--label=LABEL (workspace-extension) Sidebar label. Defaults to Title Case of the name.
|
|
44
|
+
--section=SECTION (workspace-extension) Nav section header. Defaults to "Extensions".
|
|
45
|
+
--with-api (workspace-extension) Also write an <name>-api.mjs register(addRoute) stub.
|
|
36
46
|
-h, --help Show this help.
|
|
37
47
|
|
|
38
48
|
Exit codes:
|
|
@@ -40,11 +50,19 @@ Exit codes:
|
|
|
40
50
|
`;
|
|
41
51
|
|
|
42
52
|
/**
|
|
43
|
-
* Entry point invoked by bin/pa-init. Returns nothing
|
|
53
|
+
* Entry point invoked by bin/pa-init. Returns nothing; exits the process
|
|
44
54
|
* directly with the resolved code. Throws are translated to exit code 3
|
|
45
55
|
* (transport/IO) if they carry no `exitCode`; otherwise to the carried code.
|
|
46
56
|
*/
|
|
47
57
|
export async function main(argv) {
|
|
58
|
+
// Sub-verb routing: a leading positional that names a known sub-verb
|
|
59
|
+
// routes to that command's own parser. This keeps the existing
|
|
60
|
+
// flag-driven `--mode=solo` shape backwards compatible (its argv never
|
|
61
|
+
// starts with a positional under `allowPositionals: false`).
|
|
62
|
+
if (argv[0] === 'workspace-extension') {
|
|
63
|
+
return runWorkspaceExtensionVerb(argv.slice(1));
|
|
64
|
+
}
|
|
65
|
+
|
|
48
66
|
let parsed;
|
|
49
67
|
try {
|
|
50
68
|
parsed = parseArgs({
|
|
@@ -113,3 +131,62 @@ export async function main(argv) {
|
|
|
113
131
|
function exitWith(code) {
|
|
114
132
|
process.exit(code);
|
|
115
133
|
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Parse and dispatch the `workspace-extension` sub-verb. The scaffolder's
|
|
137
|
+
* primary flow uses flag-driven mode routing; this verb takes a required
|
|
138
|
+
* positional (the extension name) so it gets its own parser.
|
|
139
|
+
*/
|
|
140
|
+
async function runWorkspaceExtensionVerb(rest) {
|
|
141
|
+
let parsed;
|
|
142
|
+
try {
|
|
143
|
+
parsed = parseArgs({
|
|
144
|
+
args: rest,
|
|
145
|
+
options: {
|
|
146
|
+
help: { type: 'boolean', short: 'h' },
|
|
147
|
+
label: { type: 'string' },
|
|
148
|
+
section: { type: 'string' },
|
|
149
|
+
'with-api': { type: 'boolean' },
|
|
150
|
+
target: { type: 'string' },
|
|
151
|
+
},
|
|
152
|
+
strict: true,
|
|
153
|
+
allowPositionals: true,
|
|
154
|
+
});
|
|
155
|
+
} catch (err) {
|
|
156
|
+
process.stderr.write(`pa-init workspace-extension: ${err.message}\n`);
|
|
157
|
+
process.stderr.write(USAGE);
|
|
158
|
+
return exitWith(1);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (parsed.values.help) {
|
|
162
|
+
process.stdout.write(USAGE);
|
|
163
|
+
return exitWith(0);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const positionals = parsed.positionals;
|
|
167
|
+
if (positionals.length === 0) {
|
|
168
|
+
process.stderr.write('pa-init workspace-extension: <name> is required\n');
|
|
169
|
+
process.stderr.write(USAGE);
|
|
170
|
+
return exitWith(1);
|
|
171
|
+
}
|
|
172
|
+
if (positionals.length > 1) {
|
|
173
|
+
process.stderr.write(
|
|
174
|
+
`pa-init workspace-extension: unexpected extra positionals: ${positionals.slice(1).join(' ')}\n`,
|
|
175
|
+
);
|
|
176
|
+
return exitWith(1);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const code = await runInitWorkspaceExtension({
|
|
181
|
+
name: positionals[0],
|
|
182
|
+
label: parsed.values.label,
|
|
183
|
+
section: parsed.values.section,
|
|
184
|
+
withApi: parsed.values['with-api'] === true,
|
|
185
|
+
targetDir: parsed.values.target,
|
|
186
|
+
});
|
|
187
|
+
return exitWith(code ?? 0);
|
|
188
|
+
} catch (err) {
|
|
189
|
+
process.stderr.write(`pa-init workspace-extension: ${err.message || err}\n`);
|
|
190
|
+
return exitWith(err && typeof err.exitCode === 'number' ? err.exitCode : 3);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// init-workspace-extension.mjs -- `pa-init workspace-extension <name>` orchestration.
|
|
2
|
+
//
|
|
3
|
+
// Scaffolds one workspace-extension page inside an existing operator repo:
|
|
4
|
+
//
|
|
5
|
+
// operator/extensions/workspace/<name>.html page skeleton
|
|
6
|
+
// operator/extensions/workspace/nav.json appended (created if absent)
|
|
7
|
+
// operator/extensions/workspace/<name>-api.mjs optional (--with-api)
|
|
8
|
+
//
|
|
9
|
+
// Design contract:
|
|
10
|
+
// - Idempotent. A second run with the same name warns and does not overwrite.
|
|
11
|
+
// - Nav merge preserves existing entries. Same-id collisions warn and are left
|
|
12
|
+
// alone (never silently overwritten). Malformed nav.json fails cleanly with
|
|
13
|
+
// exit 2 rather than corrupting the file.
|
|
14
|
+
// - Runs against the operator's cwd. The `--target` flag lets tests point at
|
|
15
|
+
// a temporary directory, but the default is `process.cwd()` on the theory
|
|
16
|
+
// that an operator invokes this from inside their own instance.
|
|
17
|
+
//
|
|
18
|
+
// Errors carry `exitCode`. The CLI translates to `process.exit`.
|
|
19
|
+
|
|
20
|
+
import { existsSync } from 'node:fs';
|
|
21
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
22
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
23
|
+
|
|
24
|
+
/** Kebab-case validator: 2-40 chars, lowercase letters + digits + hyphen, starts with a letter. */
|
|
25
|
+
const NAME_RE = /^[a-z][a-z0-9-]{1,39}$/;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Entry point for `pa-init workspace-extension <name>`. Returns an exit code.
|
|
29
|
+
*
|
|
30
|
+
* @param {object} options
|
|
31
|
+
* @param {string} options.name extension slug (kebab-case)
|
|
32
|
+
* @param {string} [options.label] nav label (defaults to Title Case of name)
|
|
33
|
+
* @param {string} [options.section] nav section (defaults to 'Extensions')
|
|
34
|
+
* @param {boolean} [options.withApi] also write a `<name>-api.mjs` stub
|
|
35
|
+
* @param {string} [options.targetDir] operator repo root (defaults to cwd)
|
|
36
|
+
*/
|
|
37
|
+
export async function runInitWorkspaceExtension(options) {
|
|
38
|
+
const name = String(options.name || '').trim();
|
|
39
|
+
if (!name) return usageError('workspace-extension: <name> is required');
|
|
40
|
+
if (!NAME_RE.test(name)) {
|
|
41
|
+
return usageError(
|
|
42
|
+
`workspace-extension: invalid name '${name}' (expected kebab-case, 2-40 chars, letters/digits/-, starts with a letter)`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const section = String(options.section || 'Extensions');
|
|
47
|
+
const label = String(options.label || defaultLabel(name));
|
|
48
|
+
const targetDir = resolveTargetDir(options.targetDir);
|
|
49
|
+
const extDir = join(targetDir, 'operator', 'extensions', 'workspace');
|
|
50
|
+
const htmlPath = join(extDir, `${name}.html`);
|
|
51
|
+
const navPath = join(extDir, 'nav.json');
|
|
52
|
+
const apiPath = join(extDir, `${name}-api.mjs`);
|
|
53
|
+
|
|
54
|
+
await mkdir(extDir, { recursive: true });
|
|
55
|
+
|
|
56
|
+
const written = [];
|
|
57
|
+
const skipped = [];
|
|
58
|
+
|
|
59
|
+
if (existsSync(htmlPath)) {
|
|
60
|
+
skipped.push(`${htmlPath} (page already exists)`);
|
|
61
|
+
} else {
|
|
62
|
+
await writeFile(htmlPath, buildHtmlSkeleton({ name, label }), 'utf8');
|
|
63
|
+
written.push(htmlPath);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const navResult = await mergeNavEntry({ navPath, name, label, section });
|
|
67
|
+
if (navResult.action === 'created' || navResult.action === 'appended') {
|
|
68
|
+
written.push(`${navPath} (${navResult.action})`);
|
|
69
|
+
} else {
|
|
70
|
+
skipped.push(`${navPath} (${navResult.reason})`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (options.withApi) {
|
|
74
|
+
if (existsSync(apiPath)) {
|
|
75
|
+
skipped.push(`${apiPath} (api stub already exists)`);
|
|
76
|
+
} else {
|
|
77
|
+
await writeFile(apiPath, buildApiStub({ name }), 'utf8');
|
|
78
|
+
written.push(apiPath);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
emitSummary({ written, skipped, name });
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Kebab -> Title Case: `standup` -> `Standup`, `my-jira-today` -> `My Jira Today`. */
|
|
87
|
+
export function defaultLabel(name) {
|
|
88
|
+
return name
|
|
89
|
+
.split('-')
|
|
90
|
+
.filter(Boolean)
|
|
91
|
+
.map((s) => s[0].toUpperCase() + s.slice(1))
|
|
92
|
+
.join(' ');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function resolveTargetDir(dir) {
|
|
96
|
+
const raw = dir || process.cwd();
|
|
97
|
+
return isAbsolute(raw) ? raw : resolve(process.cwd(), raw);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Read nav.json (or start empty), append a `{ id, label, href, section }`
|
|
102
|
+
* entry for the extension, and rewrite the file. Same-id collisions are
|
|
103
|
+
* left alone; malformed JSON fails cleanly (exit 2). Never silently
|
|
104
|
+
* overwrites an existing entry.
|
|
105
|
+
*
|
|
106
|
+
* Returns `{ action: 'created' | 'appended' | 'skipped', reason?: string }`.
|
|
107
|
+
*/
|
|
108
|
+
export async function mergeNavEntry({ navPath, name, label, section }) {
|
|
109
|
+
const entry = {
|
|
110
|
+
id: name,
|
|
111
|
+
label,
|
|
112
|
+
href: `/${name}.html`,
|
|
113
|
+
section,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
let entries;
|
|
117
|
+
let existed = false;
|
|
118
|
+
if (existsSync(navPath)) {
|
|
119
|
+
existed = true;
|
|
120
|
+
let raw;
|
|
121
|
+
try {
|
|
122
|
+
raw = await readFile(navPath, 'utf8');
|
|
123
|
+
} catch (err) {
|
|
124
|
+
const e = new Error(`workspace-extension: could not read ${navPath}: ${err.message}`);
|
|
125
|
+
e.exitCode = 3;
|
|
126
|
+
throw e;
|
|
127
|
+
}
|
|
128
|
+
let parsed;
|
|
129
|
+
try {
|
|
130
|
+
parsed = JSON.parse(raw);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
const e = new Error(
|
|
133
|
+
`workspace-extension: ${navPath} is not valid JSON (${err.message}); refusing to overwrite`,
|
|
134
|
+
);
|
|
135
|
+
e.exitCode = 2;
|
|
136
|
+
throw e;
|
|
137
|
+
}
|
|
138
|
+
if (!Array.isArray(parsed)) {
|
|
139
|
+
const e = new Error(
|
|
140
|
+
`workspace-extension: ${navPath} must be a JSON array of nav entries; refusing to overwrite`,
|
|
141
|
+
);
|
|
142
|
+
e.exitCode = 2;
|
|
143
|
+
throw e;
|
|
144
|
+
}
|
|
145
|
+
entries = parsed;
|
|
146
|
+
} else {
|
|
147
|
+
entries = [];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const collision = entries.find((it) => it && it.id === name);
|
|
151
|
+
if (collision) {
|
|
152
|
+
return {
|
|
153
|
+
action: 'skipped',
|
|
154
|
+
reason: `nav entry id '${name}' already registered (left as-is)`,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
entries.push(entry);
|
|
159
|
+
await mkdir(dirname(navPath), { recursive: true });
|
|
160
|
+
await writeFile(navPath, JSON.stringify(entries, null, 2) + '\n', 'utf8');
|
|
161
|
+
return { action: existed ? 'appended' : 'created' };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The static-page skeleton written for `<name>.html`. Kept minimal on
|
|
166
|
+
* purpose: the operator's PA fills in the real content on the next
|
|
167
|
+
* dispatch, and a heavier template would just be deleted.
|
|
168
|
+
*/
|
|
169
|
+
export function buildHtmlSkeleton({ name, label }) {
|
|
170
|
+
return `<!doctype html>
|
|
171
|
+
<html lang="en">
|
|
172
|
+
<head>
|
|
173
|
+
<meta charset="utf-8" />
|
|
174
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
175
|
+
<title>${escapeHtml(label)}</title>
|
|
176
|
+
<link rel="stylesheet" href="/core/tools/workspace/app/shell.css" />
|
|
177
|
+
</head>
|
|
178
|
+
<body>
|
|
179
|
+
<main class="page">
|
|
180
|
+
<h1>${escapeHtml(label)}</h1>
|
|
181
|
+
<!-- workspace-extension:${name} -->
|
|
182
|
+
<p>Replace this with the real page. Data lives under <code>/__api/${name}/...</code> if you scaffolded the api stub.</p>
|
|
183
|
+
</main>
|
|
184
|
+
</body>
|
|
185
|
+
</html>
|
|
186
|
+
`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* The `<name>-api.mjs` stub. Exports `register(addRoute)` and wires one
|
|
191
|
+
* placeholder GET route so a fresh scaffold responds 200 without further
|
|
192
|
+
* work. The operator replaces the handler when they build the real page.
|
|
193
|
+
*/
|
|
194
|
+
export function buildApiStub({ name }) {
|
|
195
|
+
return `// ${name}-api.mjs -- workspace-extension API routes for /${name}.html.
|
|
196
|
+
//
|
|
197
|
+
// Exports \`register(addRoute)\`; the workspace server imports this at
|
|
198
|
+
// startup and calls it. Pass \`{ crossOrigin: true }\` as a fourth
|
|
199
|
+
// addRoute argument only for routes that legitimately accept requests
|
|
200
|
+
// from another origin (e.g. a bookmarklet); the default guards Origin
|
|
201
|
+
// and Sec-Fetch-Site.
|
|
202
|
+
|
|
203
|
+
export function register(addRoute) {
|
|
204
|
+
addRoute('GET', '/__api/${name}/ping', (_req, res) => {
|
|
205
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
206
|
+
res.end(JSON.stringify({ ok: true, extension: '${name}' }));
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function escapeHtml(s) {
|
|
213
|
+
return String(s)
|
|
214
|
+
.replace(/&/g, '&')
|
|
215
|
+
.replace(/</g, '<')
|
|
216
|
+
.replace(/>/g, '>')
|
|
217
|
+
.replace(/"/g, '"');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function emitSummary({ written, skipped, name }) {
|
|
221
|
+
process.stdout.write(`pa-init workspace-extension: ${name}\n`);
|
|
222
|
+
for (const w of written) process.stdout.write(` wrote ${w}\n`);
|
|
223
|
+
for (const s of skipped) process.stdout.write(` skipped ${s}\n`);
|
|
224
|
+
if (skipped.length === 0) {
|
|
225
|
+
process.stdout.write('\nNext: fill in the page content, restart the workspace to pick up the nav entry.\n');
|
|
226
|
+
} else {
|
|
227
|
+
process.stdout.write('\nAt least one file was left as-is. Rerun after resolving, or delete the existing file first.\n');
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function usageError(message) {
|
|
232
|
+
process.stderr.write(`${message}\n`);
|
|
233
|
+
return 1;
|
|
234
|
+
}
|
|
@@ -85,16 +85,17 @@ function writeNextStepMessage(config) {
|
|
|
85
85
|
const { paName, targetDir } = config;
|
|
86
86
|
const lines = [
|
|
87
87
|
'',
|
|
88
|
-
'Next step
|
|
88
|
+
'Next step - start your PA:',
|
|
89
89
|
'',
|
|
90
90
|
` cd ${targetDir}`,
|
|
91
|
-
' pa session',
|
|
91
|
+
' pnpm exec pa session',
|
|
92
92
|
'',
|
|
93
|
-
'Optional: add a shell alias
|
|
93
|
+
'Optional: add a one-word shell alias so you can start a session from anywhere.',
|
|
94
|
+
'Add this line to ~/.zshrc (zsh) or ~/.bashrc (bash), then reload the shell:',
|
|
94
95
|
'',
|
|
95
|
-
` alias ${paName}='pa session'`,
|
|
96
|
+
` alias ${paName}='cd ${targetDir} && pnpm exec pa session'`,
|
|
96
97
|
'',
|
|
97
|
-
`After reloading
|
|
98
|
+
`After reloading, typing \`${paName}\` starts a session.`,
|
|
98
99
|
'',
|
|
99
100
|
];
|
|
100
101
|
process.stdout.write(lines.join('\n'));
|