@pugi/cli 0.1.0-alpha.9 → 0.1.0-beta.10

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 (74) hide show
  1. package/README.md +33 -0
  2. package/THIRD_PARTY_NOTICES.md +40 -0
  3. package/assets/pugi-mascot.ansi +16 -0
  4. package/dist/commands/deploy.js +439 -0
  5. package/dist/core/agents/loader.js +104 -0
  6. package/dist/core/agents/registry.js +1 -1
  7. package/dist/core/consensus/anvil-fanout.js +276 -0
  8. package/dist/core/consensus/diff-capture.js +382 -0
  9. package/dist/core/consensus/rubric.js +233 -0
  10. package/dist/core/context/index.js +21 -0
  11. package/dist/core/context/pugiignore.js +316 -0
  12. package/dist/core/context/repo-skeleton.js +533 -0
  13. package/dist/core/context/watcher.js +342 -0
  14. package/dist/core/context/working-set.js +165 -0
  15. package/dist/core/edits/dispatch.js +185 -0
  16. package/dist/core/edits/index.js +15 -0
  17. package/dist/core/edits/layer-a-apply.js +217 -0
  18. package/dist/core/edits/layer-b-apply.js +211 -0
  19. package/dist/core/edits/layer-c-apply.js +160 -0
  20. package/dist/core/edits/layer-d-ast.js +29 -0
  21. package/dist/core/edits/marker-parser.js +401 -0
  22. package/dist/core/edits/security-gate.js +223 -0
  23. package/dist/core/edits/worktree.js +322 -0
  24. package/dist/core/engine/native-pugi.js +6 -1
  25. package/dist/core/engine/prompts.js +8 -0
  26. package/dist/core/engine/tool-bridge.js +33 -1
  27. package/dist/core/lsp/client.js +719 -0
  28. package/dist/core/repl/ask.js +512 -0
  29. package/dist/core/repl/cancellation.js +98 -0
  30. package/dist/core/repl/dispatch-fsm.js +220 -0
  31. package/dist/core/repl/privacy-banner.js +71 -0
  32. package/dist/core/repl/session.js +1908 -13
  33. package/dist/core/repl/slash-commands.js +92 -32
  34. package/dist/core/repl/store/index.js +12 -0
  35. package/dist/core/repl/store/jsonl-log.js +321 -0
  36. package/dist/core/repl/store/lockfile.js +155 -0
  37. package/dist/core/repl/store/session-store.js +792 -0
  38. package/dist/core/repl/store/types.js +44 -0
  39. package/dist/core/repl/store/uuid-v7.js +68 -0
  40. package/dist/core/repl/workspace-context.js +72 -1
  41. package/dist/core/skills/defaults.js +457 -0
  42. package/dist/core/skills/loader.js +454 -0
  43. package/dist/core/skills/sources.js +480 -0
  44. package/dist/core/skills/trust.js +172 -0
  45. package/dist/runtime/cli.js +998 -12
  46. package/dist/runtime/commands/agents.js +385 -0
  47. package/dist/runtime/commands/config.js +338 -8
  48. package/dist/runtime/commands/delegate.js +289 -0
  49. package/dist/runtime/commands/lsp.js +206 -0
  50. package/dist/runtime/commands/patch.js +128 -0
  51. package/dist/runtime/commands/review-consensus.js +399 -0
  52. package/dist/runtime/commands/roster.js +117 -0
  53. package/dist/runtime/commands/skills.js +401 -0
  54. package/dist/runtime/commands/worktree.js +177 -0
  55. package/dist/runtime/plan-decompose.js +531 -0
  56. package/dist/tools/apply-patch.js +495 -0
  57. package/dist/tools/file-tools.js +90 -0
  58. package/dist/tools/lsp-tools.js +189 -0
  59. package/dist/tools/registry.js +26 -0
  60. package/dist/tools/web-fetch.js +1 -1
  61. package/dist/tui/agent-tree-pane.js +9 -0
  62. package/dist/tui/ask-cli.js +52 -0
  63. package/dist/tui/ask-modal.js +211 -0
  64. package/dist/tui/conversation-pane.js +48 -3
  65. package/dist/tui/input-box.js +48 -5
  66. package/dist/tui/markdown-render.js +266 -0
  67. package/dist/tui/repl-render.js +319 -3
  68. package/dist/tui/repl-splash-mascot.js +130 -0
  69. package/dist/tui/repl-splash.js +7 -1
  70. package/dist/tui/repl.js +96 -12
  71. package/dist/tui/status-bar.js +63 -3
  72. package/dist/tui/tool-stream-pane.js +91 -0
  73. package/docs/examples/codegraph.mcp.json +10 -0
  74. package/package.json +14 -6
package/README.md CHANGED
@@ -94,12 +94,45 @@ pugi review --triple --remote
94
94
  pugi handoff --web # hand the session off to the cabinet
95
95
  pugi sessions # list sessions from .pugi/index.json
96
96
  pugi sessions --rebuild # rebuild the index from events.jsonl
97
+ pugi deploy --target vercel my-vercel-project --project proj_42
98
+ # trigger a Vercel deploy from the bound Git source
99
+ pugi deploy --status dep_42 # vendor-agnostic status snapshot
100
+ pugi deploy --logs dep_42 --tail
101
+ # build-log tail; --tail polls until terminal
97
102
  pugi doctor --json # environment diagnostic
98
103
  pugi version # CLI version
99
104
  ```
100
105
 
101
106
  Run `pugi --help` for the full list.
102
107
 
108
+ ### Deploy
109
+
110
+ `pugi deploy` triggers a deployment for the project bound to your Vercel or
111
+ Render integration. Source is resolved from the existing `ProjectGitBinding`
112
+ row on the admin-api side — the CLI never reads local files.
113
+
114
+ ```bash
115
+ # Vercel — production (default)
116
+ pugi deploy --target vercel my-vercel-project --project proj_42
117
+
118
+ # Vercel — preview from a feature branch
119
+ pugi deploy --target vercel my-vercel-project --project proj_42 \
120
+ --target-env preview --ref feat/landing-tweaks
121
+
122
+ # Render (Sprint 2 — endpoint returns 501 today)
123
+ pugi deploy --target render my-render-svc --project proj_42
124
+
125
+ # Query existing deploys
126
+ pugi deploy --status dep_42
127
+ pugi deploy --logs dep_42 --tail
128
+ ```
129
+
130
+ Exit codes:
131
+
132
+ - `0` success (queued, ready, building)
133
+ - `1` failed (status=error, refused, not authenticated)
134
+ - `2` transient (rate-limited, runtime down, retry-safe)
135
+
103
136
  ## Privacy
104
137
 
105
138
  Pugi defaults to `local-only` — no upload happens without an explicit flag.
@@ -0,0 +1,40 @@
1
+ # Third-Party Notices
2
+
3
+ `@pugi/cli` bundles default skill content from third parties on `pugi init`.
4
+ Each entry below carries the full upstream MIT license text required for
5
+ redistribution.
6
+
7
+ ## mattpocock/skills
8
+
9
+ Pugi bundles the following skill bodies (ported, attribution preserved in
10
+ each `SKILL.md` header) from https://github.com/mattpocock/skills:
11
+
12
+ - `zoom-out` — `skills/engineering/zoom-out/SKILL.md`
13
+ - `diagnose` — `skills/engineering/diagnose/SKILL.md`
14
+ - `grill-me` — `skills/productivity/grill-me/SKILL.md`
15
+
16
+ Upstream license follows verbatim.
17
+
18
+ ---
19
+
20
+ MIT License
21
+
22
+ Copyright (c) 2026 Matt Pocock
23
+
24
+ Permission is hereby granted, free of charge, to any person obtaining a copy
25
+ of this software and associated documentation files (the "Software"), to deal
26
+ in the Software without restriction, including without limitation the rights
27
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
28
+ copies of the Software, and to permit persons to whom the Software is
29
+ furnished to do so, subject to the following conditions:
30
+
31
+ The above copyright notice and this permission notice shall be included in all
32
+ copies or substantial portions of the Software.
33
+
34
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
35
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
36
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
37
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
38
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
39
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
40
+ SOFTWARE.
@@ -0,0 +1,16 @@
1
+ [?25l 
2
+ 
3
+ 
4
+ ▄▄▄▄▄▄▄▄▄▄▄▄▄▄ 
5
+ ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ 
6
+ ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ 
7
+ ▄▄▄▄▄▄▄▄▄▄▄▄ 
8
+ ▄▄▄▄▄▄▄▄▄▄▄▄ 
9
+ ▄▄▄▄▄▄▄▄▄▄▄ 
10
+ ▄▄▄▄▄▄ 
11
+ ▄▄▄▄ ▄▄ 
12
+ ▄ ▄ ▄▄▄ 
13
+ ▄ 
14
+ 
15
+ 
16
+ [?25h
@@ -0,0 +1,439 @@
1
+ /**
2
+ * `pugi deploy` command surface — Wave 3 P2 (Task #34, 2026-05-25).
3
+ *
4
+ * Subcommands:
5
+ * pugi deploy --target vercel <vercelProject> [--project <id>] [--ref <ref>] [--target-env production|preview] [--integration <id>]
6
+ * pugi deploy --target render <renderService> [--project <id>] [--ref <ref>]
7
+ * pugi deploy --status <deployment-id>
8
+ * pugi deploy --logs <deployment-id> [--tail]
9
+ *
10
+ * Exit codes (per task spec):
11
+ * 0 success (trigger queued, status terminal=ready, logs printed)
12
+ * 1 deployment failed (status=error or trigger refused)
13
+ * 2 transient (rate-limited, runtime down, retry-safe)
14
+ *
15
+ * Project resolution:
16
+ * `--project <id>` is required for trigger commands because the CLI's
17
+ * credential store is tenant-scoped but not project-scoped. A future
18
+ * sprint (Wave 4) auto-detects the project from `.pugi/settings.json`
19
+ * bound to the workspace; until then the operator passes it explicitly.
20
+ *
21
+ * Local-first invariant (ADR-0037):
22
+ * `pugi deploy` does NOT read repository files. The Vercel deploy
23
+ * resolves source from the existing ProjectGitBinding row on the
24
+ * admin-api side — the CLI is only a request initiator + status poller.
25
+ */
26
+ import { fetchDeployStatus, fetchDeployLogs, submitDeployTrigger, } from '@pugi/sdk';
27
+ /**
28
+ * Parse `pugi deploy ...` flags + positionals into a structured shape.
29
+ * Exported so tests can exercise the parser in isolation.
30
+ */
31
+ export function parseDeployArgs(args) {
32
+ const flags = {};
33
+ const positional = [];
34
+ for (let i = 0; i < args.length; i++) {
35
+ const arg = args[i] ?? '';
36
+ if (arg === '--json') {
37
+ flags.json = true;
38
+ }
39
+ else if (arg === '--target') {
40
+ const next = args[i + 1];
41
+ if (!next)
42
+ return { flags, positional, error: '--target requires a value (vercel|render)' };
43
+ if (next !== 'vercel' && next !== 'render') {
44
+ return { flags, positional, error: `--target must be vercel or render, got "${next}"` };
45
+ }
46
+ flags.target = next;
47
+ i += 1;
48
+ }
49
+ else if (arg.startsWith('--target=')) {
50
+ const value = arg.slice('--target='.length);
51
+ if (value !== 'vercel' && value !== 'render') {
52
+ return { flags, positional, error: `--target must be vercel or render, got "${value}"` };
53
+ }
54
+ flags.target = value;
55
+ }
56
+ else if (arg === '--project') {
57
+ const next = args[i + 1];
58
+ if (!next)
59
+ return { flags, positional, error: '--project requires a value' };
60
+ flags.project = next;
61
+ i += 1;
62
+ }
63
+ else if (arg.startsWith('--project=')) {
64
+ flags.project = arg.slice('--project='.length);
65
+ }
66
+ else if (arg === '--ref') {
67
+ const next = args[i + 1];
68
+ if (!next)
69
+ return { flags, positional, error: '--ref requires a value' };
70
+ flags.ref = next;
71
+ i += 1;
72
+ }
73
+ else if (arg.startsWith('--ref=')) {
74
+ flags.ref = arg.slice('--ref='.length);
75
+ }
76
+ else if (arg === '--target-env') {
77
+ const next = args[i + 1];
78
+ if (!next)
79
+ return { flags, positional, error: '--target-env requires a value' };
80
+ if (next !== 'production' && next !== 'preview') {
81
+ return {
82
+ flags,
83
+ positional,
84
+ error: `--target-env must be production or preview, got "${next}"`,
85
+ };
86
+ }
87
+ flags.targetEnv = next;
88
+ i += 1;
89
+ }
90
+ else if (arg.startsWith('--target-env=')) {
91
+ const value = arg.slice('--target-env='.length);
92
+ if (value !== 'production' && value !== 'preview') {
93
+ return {
94
+ flags,
95
+ positional,
96
+ error: `--target-env must be production or preview, got "${value}"`,
97
+ };
98
+ }
99
+ flags.targetEnv = value;
100
+ }
101
+ else if (arg === '--integration') {
102
+ const next = args[i + 1];
103
+ if (!next)
104
+ return { flags, positional, error: '--integration requires a value' };
105
+ flags.integration = next;
106
+ i += 1;
107
+ }
108
+ else if (arg.startsWith('--integration=')) {
109
+ flags.integration = arg.slice('--integration='.length);
110
+ }
111
+ else if (arg === '--status') {
112
+ const next = args[i + 1];
113
+ if (!next)
114
+ return { flags, positional, error: '--status requires a deployment id' };
115
+ flags.status = next;
116
+ i += 1;
117
+ }
118
+ else if (arg.startsWith('--status=')) {
119
+ flags.status = arg.slice('--status='.length);
120
+ }
121
+ else if (arg === '--logs') {
122
+ const next = args[i + 1];
123
+ if (!next)
124
+ return { flags, positional, error: '--logs requires a deployment id' };
125
+ flags.logs = next;
126
+ i += 1;
127
+ }
128
+ else if (arg.startsWith('--logs=')) {
129
+ flags.logs = arg.slice('--logs='.length);
130
+ }
131
+ else if (arg === '--tail') {
132
+ flags.tail = true;
133
+ }
134
+ else if (arg.startsWith('--')) {
135
+ return { flags, positional, error: `unknown flag: ${arg}` };
136
+ }
137
+ else {
138
+ positional.push(arg);
139
+ }
140
+ }
141
+ return { flags, positional };
142
+ }
143
+ export function usage() {
144
+ return [
145
+ 'Usage:',
146
+ ' pugi deploy --target vercel <vercelProject> --project <id> [--target-env production|preview] [--ref <git-ref>] [--integration <id>]',
147
+ ' pugi deploy --target render <renderService> --project <id> [--ref <git-ref>]',
148
+ ' pugi deploy --status <deployment-id>',
149
+ ' pugi deploy --logs <deployment-id> [--tail]',
150
+ '',
151
+ 'Flags:',
152
+ ' --target vercel|render Provider to deploy to.',
153
+ ' --project <id> Pugi project id (required for trigger).',
154
+ ' --ref <git-ref> Optional Git ref. Defaults to the binding default branch.',
155
+ ' --target-env production|preview',
156
+ ' Vercel deploy environment. Defaults to production.',
157
+ ' --integration <id> Disambiguate when multiple integrations are active.',
158
+ ' --status <id> Show the current status for a deployment.',
159
+ ' --logs <id> [--tail] Fetch log tail. With --tail, polls until terminal.',
160
+ ' --json Emit machine-readable JSON envelopes.',
161
+ '',
162
+ 'Exit codes:',
163
+ ' 0 success',
164
+ ' 1 deployment failed / refused',
165
+ ' 2 transient (rate-limited, runtime down, retry-safe)',
166
+ ].join('\n');
167
+ }
168
+ /**
169
+ * Main entry point. Returns the exit code; the CLI shim sets
170
+ * `process.exitCode = result` so tests can drive this without a child
171
+ * process and the REPL can poll the value.
172
+ */
173
+ export async function runDeployCommand(args, io, deps) {
174
+ const parsed = parseDeployArgs(args);
175
+ if (parsed.error) {
176
+ if (parsed.flags.json) {
177
+ io.write(`${JSON.stringify({ command: 'deploy', ok: false, error: parsed.error }, null, 2)}\n`);
178
+ }
179
+ else {
180
+ io.writeError(parsed.error);
181
+ io.writeError(usage());
182
+ }
183
+ return 2;
184
+ }
185
+ const { flags, positional } = parsed;
186
+ // Branch 1: --status <id>
187
+ if (flags.status) {
188
+ return runStatus(flags.status, flags, io, deps);
189
+ }
190
+ // Branch 2: --logs <id>
191
+ if (flags.logs) {
192
+ return runLogs(flags.logs, flags, io, deps);
193
+ }
194
+ // Branch 3: trigger via --target <vercel|render>
195
+ if (!flags.target) {
196
+ if (flags.json) {
197
+ io.write(`${JSON.stringify({
198
+ command: 'deploy',
199
+ ok: false,
200
+ error: 'No --target specified. Use --target vercel|render, or --status / --logs to query existing deploys.',
201
+ }, null, 2)}\n`);
202
+ }
203
+ else {
204
+ io.writeError('No --target specified. Use --target vercel|render, or --status / --logs to query existing deploys.');
205
+ io.writeError(usage());
206
+ }
207
+ return 2;
208
+ }
209
+ const projectKey = positional[0];
210
+ if (!projectKey) {
211
+ const msg = flags.target === 'vercel'
212
+ ? 'pugi deploy --target vercel requires a Vercel project name as the positional argument'
213
+ : 'pugi deploy --target render requires a Render service name as the positional argument';
214
+ if (flags.json) {
215
+ io.write(`${JSON.stringify({ command: 'deploy', ok: false, error: msg }, null, 2)}\n`);
216
+ }
217
+ else {
218
+ io.writeError(msg);
219
+ io.writeError(usage());
220
+ }
221
+ return 2;
222
+ }
223
+ if (!flags.project) {
224
+ const msg = 'pugi deploy trigger requires --project <pugi-project-id>';
225
+ if (flags.json) {
226
+ io.write(`${JSON.stringify({ command: 'deploy', ok: false, error: msg }, null, 2)}\n`);
227
+ }
228
+ else {
229
+ io.writeError(msg);
230
+ io.writeError(usage());
231
+ }
232
+ return 2;
233
+ }
234
+ const config = deps.resolveConfig();
235
+ if (!config) {
236
+ const msg = 'Not authenticated. Run `pugi login` first.';
237
+ if (flags.json) {
238
+ io.write(`${JSON.stringify({ command: 'deploy', ok: false, error: msg }, null, 2)}\n`);
239
+ }
240
+ else {
241
+ io.writeError(msg);
242
+ }
243
+ return 1;
244
+ }
245
+ let result;
246
+ if (flags.target === 'vercel') {
247
+ result = await submitDeployTrigger(config, 'vercel', {
248
+ schema: 1,
249
+ projectId: flags.project,
250
+ vercelProject: projectKey,
251
+ ...(flags.integration ? { integrationId: flags.integration } : {}),
252
+ ...(flags.targetEnv ? { target: flags.targetEnv } : {}),
253
+ ...(flags.ref ? { ref: flags.ref } : {}),
254
+ });
255
+ }
256
+ else {
257
+ result = await submitDeployTrigger(config, 'render', {
258
+ schema: 1,
259
+ projectId: flags.project,
260
+ renderService: projectKey,
261
+ ...(flags.ref ? { ref: flags.ref } : {}),
262
+ });
263
+ }
264
+ return emitTriggerResult(result, flags, io);
265
+ }
266
+ function emitTriggerResult(result, flags, io) {
267
+ if (result.status === 'ok') {
268
+ if (flags.json) {
269
+ io.write(`${JSON.stringify({ command: 'deploy.trigger', ok: true, deployment: result.response }, null, 2)}\n`);
270
+ }
271
+ else {
272
+ io.write(renderStatusTable(result.response, 'queued'));
273
+ }
274
+ return 0;
275
+ }
276
+ return emitDeployFailure('deploy.trigger', result, flags, io);
277
+ }
278
+ async function runStatus(deploymentId, flags, io, deps) {
279
+ const config = deps.resolveConfig();
280
+ if (!config) {
281
+ const msg = 'Not authenticated. Run `pugi login` first.';
282
+ if (flags.json) {
283
+ io.write(`${JSON.stringify({ command: 'deploy.status', ok: false, error: msg }, null, 2)}\n`);
284
+ }
285
+ else {
286
+ io.writeError(msg);
287
+ }
288
+ return 1;
289
+ }
290
+ const result = await fetchDeployStatus(config, deploymentId);
291
+ if (result.status === 'ok') {
292
+ if (flags.json) {
293
+ io.write(`${JSON.stringify({ command: 'deploy.status', ok: true, deployment: result.response }, null, 2)}\n`);
294
+ }
295
+ else {
296
+ io.write(renderStatusTable(result.response, 'current'));
297
+ }
298
+ return statusToExitCode(result.response.status);
299
+ }
300
+ return emitDeployFailure('deploy.status', result, flags, io);
301
+ }
302
+ async function runLogs(deploymentId, flags, io, deps) {
303
+ const config = deps.resolveConfig();
304
+ if (!config) {
305
+ const msg = 'Not authenticated. Run `pugi login` first.';
306
+ if (flags.json) {
307
+ io.write(`${JSON.stringify({ command: 'deploy.logs', ok: false, error: msg }, null, 2)}\n`);
308
+ }
309
+ else {
310
+ io.writeError(msg);
311
+ }
312
+ return 1;
313
+ }
314
+ const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
315
+ let lastTerminal = false;
316
+ let lastStatus = 'queued';
317
+ let lastLogs = '';
318
+ // Single-shot vs --tail. The tail loop is bounded by the 10-minute hard
319
+ // ceiling so a runaway runtime doesn't keep a CI job pinned forever.
320
+ const startedAt = Date.now();
321
+ const maxMs = 10 * 60 * 1000;
322
+ const pollIntervalMs = 3_000;
323
+ while (true) {
324
+ const result = await fetchDeployLogs(config, deploymentId);
325
+ if (result.status !== 'ok') {
326
+ return emitDeployFailure('deploy.logs', result, flags, io);
327
+ }
328
+ const { response } = result;
329
+ lastTerminal = response.terminal;
330
+ lastStatus = response.status;
331
+ lastLogs = response.logs;
332
+ if (flags.json) {
333
+ io.write(`${JSON.stringify({ command: 'deploy.logs', ok: true, snapshot: response }, null, 2)}\n`);
334
+ }
335
+ else {
336
+ io.write(response.logs.endsWith('\n') ? response.logs : `${response.logs}\n`);
337
+ }
338
+ if (!flags.tail)
339
+ break;
340
+ if (response.terminal)
341
+ break;
342
+ if (Date.now() - startedAt > maxMs) {
343
+ io.writeError(`--tail timed out after ${Math.floor(maxMs / 60_000)} minutes; deployment still ${response.status}. Re-run with --status to confirm.`);
344
+ return 2;
345
+ }
346
+ await sleep(pollIntervalMs);
347
+ }
348
+ // Suppress unused-warning safe-guards in single-shot path.
349
+ void lastTerminal;
350
+ void lastLogs;
351
+ return statusToExitCode(lastStatus);
352
+ }
353
+ function emitDeployFailure(command, result, flags, io) {
354
+ if (result.status === 'ok')
355
+ return 0;
356
+ if (flags.json) {
357
+ io.write(`${JSON.stringify({ command, ok: false, ...result }, null, 2)}\n`);
358
+ }
359
+ switch (result.status) {
360
+ case 'unauthenticated':
361
+ if (!flags.json) {
362
+ io.writeError('Runtime rejected credentials. Re-run `pugi login` or refresh your API key.');
363
+ }
364
+ return 1;
365
+ case 'endpoint_missing':
366
+ if (!flags.json) {
367
+ io.writeError('Deploy endpoint not deployed on this runtime. Update your runtime or set PUGI_API_URL to a runtime that supports /api/pugi/deploy/*.');
368
+ }
369
+ return 2;
370
+ case 'not_configured':
371
+ if (!flags.json) {
372
+ io.writeError('Provider not configured on this runtime. Try `pugi deploy --target vercel` until Render ships in Sprint 2.');
373
+ }
374
+ return 2;
375
+ case 'not_found':
376
+ if (!flags.json) {
377
+ io.writeError('Deployment not found for this tenant.');
378
+ }
379
+ return 1;
380
+ case 'rate_limited':
381
+ if (!flags.json) {
382
+ const retrySec = Math.max(1, Math.ceil(result.retryAfterMs / 1000));
383
+ io.writeError(`Runtime rate-limited. Retry in ${retrySec}s. (Vercel Hobby cap is the common cause.)`);
384
+ }
385
+ return 2;
386
+ case 'failed':
387
+ if (!flags.json) {
388
+ io.writeError(`Deploy call failed: ${result.message}`);
389
+ }
390
+ // Network / 5xx → transient. Schema validation / 4xx → permanent.
391
+ // We cannot perfectly disambiguate here, so map HTTP < 500 to 1
392
+ // (caller error, fix and retry) and the rest to 2 (transient).
393
+ return result.code >= 500 || result.code === 0 ? 2 : 1;
394
+ default:
395
+ // Exhaustiveness: TS catches new variants at compile time. Runtime
396
+ // fall-through keeps the CLI from hanging if a future SDK version
397
+ // adds a status the CLI doesn't yet know about.
398
+ if (!flags.json) {
399
+ io.writeError(`Unknown deploy result.`);
400
+ }
401
+ return 2;
402
+ }
403
+ }
404
+ function statusToExitCode(status) {
405
+ switch (status) {
406
+ case 'ready':
407
+ case 'queued':
408
+ case 'building':
409
+ return 0;
410
+ case 'error':
411
+ return 1;
412
+ case 'canceled':
413
+ return 1;
414
+ default:
415
+ return 0;
416
+ }
417
+ }
418
+ function renderStatusTable(response, context) {
419
+ const lines = [];
420
+ lines.push(context === 'queued' ? 'Deployment queued.' : 'Deployment status:');
421
+ lines.push(` id: ${response.id}`);
422
+ lines.push(` provider: ${response.provider}`);
423
+ lines.push(` project: ${response.projectId}`);
424
+ lines.push(` status: ${response.status}`);
425
+ if (response.url)
426
+ lines.push(` url: ${response.url}`);
427
+ if (response.providerDeploymentId)
428
+ lines.push(` providerDeploymentId: ${response.providerDeploymentId}`);
429
+ lines.push(` createdAt: ${response.createdAt}`);
430
+ lines.push(` updatedAt: ${response.updatedAt}`);
431
+ if (response.error) {
432
+ lines.push('');
433
+ lines.push('--- build error tail ---');
434
+ lines.push(response.error);
435
+ }
436
+ lines.push('');
437
+ return lines.join('\n');
438
+ }
439
+ //# sourceMappingURL=deploy.js.map
@@ -0,0 +1,104 @@
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ import { assertValidSlug, parseSkillMarkdown } from '../skills/loader.js';
5
+ export function globalAgentsDir() {
6
+ const home = process.env.PUGI_HOME ?? resolve(homedir(), '.pugi');
7
+ return join(home, 'agents');
8
+ }
9
+ export function workspaceAgentsDir(workspaceRoot) {
10
+ return join(workspaceRoot, '.pugi', 'agents');
11
+ }
12
+ export function globalAgentPath(slug) {
13
+ assertValidSlug(slug, 'agent');
14
+ return join(globalAgentsDir(), `${slug}.md`);
15
+ }
16
+ export function workspaceAgentPath(workspaceRoot, slug) {
17
+ assertValidSlug(slug, 'agent');
18
+ return join(workspaceAgentsDir(workspaceRoot), `${slug}.md`);
19
+ }
20
+ export function listAgents(scope, workspaceRoot) {
21
+ const dir = scope === 'global' ? globalAgentsDir() : workspaceAgentsDir(workspaceRoot);
22
+ if (!existsSync(dir))
23
+ return [];
24
+ return readdirSync(dir)
25
+ .filter((name) => name.endsWith('.md'))
26
+ .sort((a, b) => a.localeCompare(b))
27
+ .map((name) => loadAgent(join(dir, name), scope))
28
+ .filter((agent) => agent !== null);
29
+ }
30
+ function loadAgent(filePath, scope) {
31
+ try {
32
+ const source = readFileSync(filePath, 'utf8');
33
+ const parsed = parseSkillMarkdown(source);
34
+ // Files under `<scope>/.pugi/agents/` are agents by construction;
35
+ // the loader override here forces metadata.type=agent even when the
36
+ // upstream frontmatter (e.g. Anthropic flat dialect) omitted the
37
+ // declaration. We never mis-categorise a `<dir>/.pugi/agents/foo.md`
38
+ // file as a skill.
39
+ if (parsed.frontmatter.metadata.type !== 'agent' &&
40
+ parsed.frontmatter.metadata.type !== 'skill') {
41
+ return null;
42
+ }
43
+ const slug = filePath.split('/').pop()?.replace(/\.md$/, '') ?? parsed.frontmatter.name;
44
+ // Filenames on disk are produced by installAgent (which validates)
45
+ // OR placed manually by the operator. Validate before exposing the
46
+ // slug to the rest of the system so trust keys + log lines never
47
+ // carry a hostile string.
48
+ assertValidSlug(slug, 'agent');
49
+ const frontmatter = {
50
+ ...parsed.frontmatter,
51
+ metadata: { ...parsed.frontmatter.metadata, type: 'agent' },
52
+ };
53
+ return {
54
+ slug,
55
+ scope,
56
+ filePath,
57
+ frontmatter,
58
+ body: parsed.body,
59
+ source,
60
+ };
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ }
66
+ export function installAgent(input) {
67
+ // Fail-closed before any filesystem mutation. assertValidSlug also
68
+ // runs inside globalAgentPath/workspaceAgentPath but we surface it
69
+ // explicitly here so the error fires before mkdirSync.
70
+ assertValidSlug(input.slug, 'agent');
71
+ const target = input.scope === 'global'
72
+ ? globalAgentPath(input.slug)
73
+ : workspaceAgentPath(input.workspaceRoot, input.slug);
74
+ mkdirSync(dirname(target), { recursive: true });
75
+ const srcFile = pickAgentFile(input.payloadDir);
76
+ writeFileSync(target, readFileSync(srcFile), { mode: 0o600 });
77
+ return target;
78
+ }
79
+ function pickAgentFile(payloadDir) {
80
+ const stat = statSync(payloadDir);
81
+ if (stat.isFile())
82
+ return payloadDir;
83
+ const entries = readdirSync(payloadDir).filter((name) => name.toLowerCase().endsWith('.md'));
84
+ if (entries.length === 0) {
85
+ throw new Error('AGENT_INSTALL: payload directory contains no .md file');
86
+ }
87
+ if (entries.length > 1) {
88
+ throw new Error(`AGENT_INSTALL: payload directory contains ${entries.length} .md files (expected exactly 1)`);
89
+ }
90
+ const first = entries[0];
91
+ if (!first) {
92
+ throw new Error('AGENT_INSTALL: payload directory contains no .md file');
93
+ }
94
+ return join(payloadDir, first);
95
+ }
96
+ export function removeAgent(slug, scope, workspaceRoot) {
97
+ assertValidSlug(slug, 'agent');
98
+ const target = scope === 'global' ? globalAgentPath(slug) : workspaceAgentPath(workspaceRoot, slug);
99
+ if (!existsSync(target))
100
+ return false;
101
+ rmSync(target, { force: true });
102
+ return true;
103
+ }
104
+ //# sourceMappingURL=loader.js.map
@@ -34,7 +34,7 @@ function requirePersona(slug) {
34
34
  * pipeline already merges the two surfaces.
35
35
  */
36
36
  export const SUBAGENT_REGISTRY = [
37
- { role: 'orchestrator', persona: requirePersona('main') }, // Mira (Pug)
37
+ { role: 'orchestrator', persona: requirePersona('main') }, // Pugi (Pug)
38
38
  { role: 'architect', persona: requirePersona('architect') }, // Marcus (Owl)
39
39
  { role: 'coder', persona: requirePersona('dev') }, // Hiroshi (Wolf)
40
40
  { role: 'verifier', persona: requirePersona('qa') }, // Vera (Fox)