@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.
Files changed (92) hide show
  1. package/README.md +12 -2
  2. package/bin/confluence +9 -0
  3. package/bin/confluence-fetch +9 -0
  4. package/bin/jira +9 -0
  5. package/dist/engineering-pa-core/knowledge/content-voice/banned-ai-tells-taxonomy.md +1 -1
  6. package/dist/engineering-pa-core/knowledge/knowledge-memory/staging-and-observations.md +3 -3
  7. package/dist/engineering-pa-core/knowledge/platform-tooling/workspace-extensions.md +62 -0
  8. package/dist/engineering-pa-core-extensions-atlassian/README.md +81 -16
  9. package/dist/engineering-pa-core-extensions-atlassian/bin/confluence +2 -0
  10. package/dist/engineering-pa-core-extensions-atlassian/bin/confluence-fetch +2 -0
  11. package/dist/engineering-pa-core-extensions-atlassian/bin/jira +2 -0
  12. package/dist/engineering-pa-core-extensions-atlassian/config/jira-workflow-chains.default.json +4 -0
  13. package/dist/engineering-pa-core-extensions-atlassian/index.mjs +17 -6
  14. package/dist/engineering-pa-core-extensions-atlassian/package.json +11 -4
  15. package/dist/engineering-pa-core-extensions-atlassian/src/common/invalidate-client.mjs +137 -0
  16. package/dist/engineering-pa-core-extensions-atlassian/src/common/pa-name.mjs +58 -0
  17. package/dist/engineering-pa-core-extensions-atlassian/src/common/secrets.mjs +158 -0
  18. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/cli.mjs +165 -0
  19. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/client.mjs +294 -0
  20. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/auth.mjs +59 -0
  21. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/comment.mjs +174 -0
  22. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/create.mjs +136 -0
  23. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/label.mjs +98 -0
  24. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/mine.mjs +105 -0
  25. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/mirror-folder.mjs +487 -0
  26. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/mirror-page.mjs +351 -0
  27. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/page.mjs +127 -0
  28. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/pages.mjs +107 -0
  29. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/recent.mjs +107 -0
  30. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/search.mjs +123 -0
  31. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/spaces.mjs +82 -0
  32. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/test.mjs +155 -0
  33. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/tree.mjs +103 -0
  34. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/update.mjs +134 -0
  35. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/format.mjs +260 -0
  36. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/mirror.mjs +353 -0
  37. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/storage.mjs +880 -0
  38. package/dist/engineering-pa-core-extensions-atlassian/src/confluence-fetch/cli.mjs +313 -0
  39. package/dist/engineering-pa-core-extensions-atlassian/src/confluence-fetch/fetch.mjs +327 -0
  40. package/dist/engineering-pa-core-extensions-atlassian/src/jira/adf.mjs +388 -0
  41. package/dist/engineering-pa-core-extensions-atlassian/src/jira/bug-fields.mjs +294 -0
  42. package/dist/engineering-pa-core-extensions-atlassian/src/jira/cli.mjs +197 -0
  43. package/dist/engineering-pa-core-extensions-atlassian/src/jira/client.mjs +350 -0
  44. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/advance.mjs +486 -0
  45. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/attach.mjs +129 -0
  46. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/auth.mjs +53 -0
  47. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/boards.mjs +88 -0
  48. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/bulk-transition.mjs +223 -0
  49. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/cancel.mjs +135 -0
  50. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/comment.mjs +169 -0
  51. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/create.mjs +376 -0
  52. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/issue.mjs +105 -0
  53. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/link.mjs +204 -0
  54. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/mine.mjs +97 -0
  55. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/projects.mjs +91 -0
  56. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/search.mjs +80 -0
  57. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/sprint.mjs +111 -0
  58. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/sync-statuses.mjs +313 -0
  59. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/test.mjs +142 -0
  60. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/transition.mjs +197 -0
  61. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/update.mjs +284 -0
  62. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/worklog.mjs +380 -0
  63. package/dist/engineering-pa-core-extensions-atlassian/src/jira/format.mjs +267 -0
  64. package/dist/engineering-pa-core-extensions-atlassian/src/jira/invalidate.mjs +72 -0
  65. package/dist/engineering-pa-core-extensions-atlassian/src/jira/metadata.mjs +225 -0
  66. package/dist/engineering-pa-core-extensions-atlassian/src/jira/required-fields.mjs +334 -0
  67. package/dist/engineering-pa-core-extensions-atlassian/src/jira/workflow-chains.mjs +405 -0
  68. package/dist/engineering-pa-core-extensions-atlassian/src/jira/workflow-map.mjs +107 -0
  69. package/dist/engineering-pa-scaffold/README.md +32 -1
  70. package/dist/engineering-pa-scaffold/src/cli.mjs +83 -6
  71. package/dist/engineering-pa-scaffold/src/commands/init-workspace-extension.mjs +234 -0
  72. package/dist/engineering-pa-scaffold/src/commands/init.mjs +6 -5
  73. package/dist/engineering-pa-scaffold/src/generator.mjs +240 -4
  74. package/dist/engineering-pa-scaffold/templates/CLAUDE.md.template +7 -7
  75. package/dist/engineering-pa-scaffold/templates/operator-README.md.template +4 -4
  76. package/dist/engineering-pa-scaffold/templates/root-CLAUDE.md.template +13 -0
  77. package/dist/engineering-pa-types-engineering/index.mjs +14 -6
  78. package/dist/engineering-pa-types-engineering/knowledge/rcf-lite/create-a-small-app.md +81 -0
  79. package/dist/engineering-pa-types-engineering/knowledge/rcf-lite/for-agents.md +25 -0
  80. package/dist/engineering-pa-types-engineering/knowledge/rcf-lite/index.md +34 -0
  81. package/dist/engineering-pa-types-engineering/knowledge/rcf-lite/reference.md +16 -0
  82. package/dist/engineering-pa-types-engineering/knowledge/rcf-lite/wire-your-agent.md +17 -0
  83. package/dist/engineering-pa-types-engineering/package.json +2 -2
  84. package/dist/engineering-pa-types-engineering/roles/rcf-lite-engineer/definition.md +183 -0
  85. package/dist/engineering-pa-workspace/app/home.html +7 -7
  86. package/dist/engineering-pa-workspace/app/onboarding.html +4 -4
  87. package/dist/engineering-pa-workspace/src/api.mjs +1 -1
  88. package/dist/engineering-pa-workspace/src/server.mjs +1 -1
  89. package/docs/installing.md +78 -0
  90. package/docs/onboarding-solo.md +162 -0
  91. package/docs/what-it-can-do.md +85 -0
  92. package/package.json +6 -2
@@ -1,4 +1,4 @@
1
- // pa-init — onboarding scaffolder.
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 — scaffold a new personal-assistant operator repo
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 — not yet implemented)
27
- member --invite=TOKEN Join an existing team library (Phase 5 — not yet implemented)
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. Must be empty or not exist.
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 — exits the process
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, '&amp;')
215
+ .replace(/</g, '&lt;')
216
+ .replace(/>/g, '&gt;')
217
+ .replace(/"/g, '&quot;');
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 — start your PA:',
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 for muscle memory. Add this to ~/.zshrc or ~/.bashrc:',
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 your shell, just type \`${paName}\` to start a session.`,
98
+ `After reloading, typing \`${paName}\` starts a session.`,
98
99
  '',
99
100
  ];
100
101
  process.stdout.write(lines.join('\n'));