@scribe-platform/cli 0.7.0 → 0.9.0

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 (65) hide show
  1. package/README.md +28 -0
  2. package/dist/commands/login.d.ts.map +1 -1
  3. package/dist/commands/login.js +15 -1
  4. package/dist/commands/login.js.map +1 -1
  5. package/dist/commands/proof.d.ts +5 -0
  6. package/dist/commands/proof.d.ts.map +1 -0
  7. package/dist/commands/proof.js +440 -0
  8. package/dist/commands/proof.js.map +1 -0
  9. package/dist/commands/prospect.d.ts +27 -0
  10. package/dist/commands/prospect.d.ts.map +1 -0
  11. package/dist/commands/prospect.js +222 -0
  12. package/dist/commands/prospect.js.map +1 -0
  13. package/dist/commands/request.d.ts +38 -0
  14. package/dist/commands/request.d.ts.map +1 -0
  15. package/dist/commands/request.js +214 -0
  16. package/dist/commands/request.js.map +1 -0
  17. package/dist/commands/site.d.ts +11 -0
  18. package/dist/commands/site.d.ts.map +1 -1
  19. package/dist/commands/site.js +41 -3
  20. package/dist/commands/site.js.map +1 -1
  21. package/dist/commands/update.d.ts +10 -0
  22. package/dist/commands/update.d.ts.map +1 -1
  23. package/dist/commands/update.js +128 -31
  24. package/dist/commands/update.js.map +1 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +15 -1
  27. package/dist/index.js.map +1 -1
  28. package/dist/lib/claude-hook.d.ts +74 -0
  29. package/dist/lib/claude-hook.d.ts.map +1 -0
  30. package/dist/lib/claude-hook.js +108 -0
  31. package/dist/lib/claude-hook.js.map +1 -0
  32. package/dist/lib/client.d.ts +351 -0
  33. package/dist/lib/client.d.ts.map +1 -1
  34. package/dist/lib/client.js +159 -0
  35. package/dist/lib/client.js.map +1 -1
  36. package/dist/lib/proof-folder.d.ts +86 -0
  37. package/dist/lib/proof-folder.d.ts.map +1 -0
  38. package/dist/lib/proof-folder.js +226 -0
  39. package/dist/lib/proof-folder.js.map +1 -0
  40. package/dist/lib/proof-preview.d.ts +39 -0
  41. package/dist/lib/proof-preview.d.ts.map +1 -0
  42. package/dist/lib/proof-preview.js +318 -0
  43. package/dist/lib/proof-preview.js.map +1 -0
  44. package/dist/lib/proof-push.d.ts +42 -0
  45. package/dist/lib/proof-push.d.ts.map +1 -0
  46. package/dist/lib/proof-push.js +91 -0
  47. package/dist/lib/proof-push.js.map +1 -0
  48. package/dist/lib/skill.d.ts +13 -0
  49. package/dist/lib/skill.d.ts.map +1 -1
  50. package/dist/lib/skill.js +33 -2
  51. package/dist/lib/skill.js.map +1 -1
  52. package/dist/lib/update-check.d.ts +46 -0
  53. package/dist/lib/update-check.d.ts.map +1 -0
  54. package/dist/lib/update-check.js +103 -0
  55. package/dist/lib/update-check.js.map +1 -0
  56. package/package.json +5 -3
  57. package/runtime/proof.js +139 -0
  58. package/skills/scribe-site-builder/SKILL.md +175 -33
  59. package/skills/scribe-site-builder/recipes/README.md +11 -3
  60. package/skills/scribe-site-builder/recipes/domains.md +151 -0
  61. package/skills/scribe-site-builder/recipes/forms.md +5 -2
  62. package/skills/scribe-site-builder/recipes/proofs.md +311 -0
  63. package/skills/scribe-site-builder/recipes/prospects.md +192 -0
  64. package/skills/scribe-site-builder/recipes/requests.md +145 -0
  65. package/skills/scribe-site-builder/recipes/schema.md +24 -0
@@ -0,0 +1,222 @@
1
+ import { CALL_OUTCOMES } from '../lib/client.js';
2
+ import { printResult, requireClient, runAction } from '../lib/cli-utils.js';
3
+ const STATUSES = ['prospect', 'active', 'churned', 'lost'];
4
+ function isValidStatus(status) {
5
+ return STATUSES.includes(status);
6
+ }
7
+ /** Mirrors the console's sort dropdown — see RFC 0027. */
8
+ const SORTS = ['due', 'neglected', 'warm', 'name'];
9
+ function isValidSort(sort) {
10
+ return SORTS.includes(sort);
11
+ }
12
+ function isValidOutcome(outcome) {
13
+ return CALL_OUTCOMES.includes(outcome);
14
+ }
15
+ /** `--next` and `--clear-next` are mutually exclusive — a reschedule is to a date or to nothing, never both. */
16
+ function resolveFollowUpUpdate(opts) {
17
+ if (opts.next && opts.clearNext) {
18
+ return { ok: false, error: '--next and --clear-next are mutually exclusive' };
19
+ }
20
+ if (opts.clearNext)
21
+ return { ok: true, value: null };
22
+ if (opts.next)
23
+ return { ok: true, value: opts.next };
24
+ return { ok: true };
25
+ }
26
+ /**
27
+ * RFC 0027 — the nurture-loop primitive. `log` is one note plus, optionally,
28
+ * one reschedule of the next follow-up; `--next`/`--clear-next` are mutually
29
+ * exclusive because a reschedule is either to a date or to nothing, never
30
+ * both. No delete subcommand here or anywhere under `/api/v1/prospects` —
31
+ * deletion stays console-only.
32
+ */
33
+ export function registerProspectCommands(program) {
34
+ const prospect = program.command('prospect').description('Manage prospects and clients in the calling pipeline');
35
+ prospect
36
+ .command('list')
37
+ .description('List prospects, optionally filtered by status or search term')
38
+ .option('--status <status>', 'prospect, active, churned, or lost')
39
+ .option('--search <text>', 'Filter by name')
40
+ .option('--due', 'Only prospects with a follow-up due now or overdue, soonest/most-overdue first')
41
+ .option('--sort <sort>', `one of: ${SORTS.join(', ')} (default: due)`)
42
+ .option('--json', 'Output as JSON')
43
+ .action(runAction(async (opts) => {
44
+ if (opts.sort && !isValidSort(opts.sort)) {
45
+ console.error(`--sort must be one of: ${SORTS.join(', ')}`);
46
+ process.exitCode = 1;
47
+ return;
48
+ }
49
+ const items = await requireClient().listProspects(opts);
50
+ if (opts.json) {
51
+ printResult(items, true);
52
+ return;
53
+ }
54
+ printResult(items.map((p) => ({
55
+ id: p.id,
56
+ name: p.name,
57
+ status: p.status,
58
+ // The attempt count travels with the warmth because `cold` means
59
+ // something different after one try than after five.
60
+ warmth: p.warmth.warmth,
61
+ tries: p.warmth.attempts,
62
+ nextFollowUp: p.prospecting?.nextFollowUpAt ?? '',
63
+ lastContact: p.warmth.lastContactAt ?? '',
64
+ })), false);
65
+ // Below the table, not as a column: these are sentences, and a column
66
+ // wide enough for one would squeeze everything that tells Ashley who
67
+ // to call. RFC 0029 — context for the call, not a change to the list.
68
+ const withProofs = items.filter((p) => p.proofNote);
69
+ if (withProofs.length > 0) {
70
+ console.log('');
71
+ for (const p of withProofs)
72
+ console.log(`${p.name} — ${p.proofNote}`);
73
+ }
74
+ }));
75
+ prospect
76
+ .command('show')
77
+ .description('Show one prospect, including its full call log')
78
+ .argument('<id>')
79
+ .option('--json', 'Output as JSON')
80
+ .action(runAction(async (id, opts) => {
81
+ const p = await requireClient().getProspect(id);
82
+ if (opts.json) {
83
+ printResult(p, true);
84
+ return;
85
+ }
86
+ console.log(`${p.name} (${p.status})`);
87
+ if (p.primaryContact) {
88
+ const contact = [p.primaryContact.name, p.primaryContact.email, p.primaryContact.phone].filter(Boolean).join(' / ');
89
+ if (contact)
90
+ console.log(`Contact: ${contact}`);
91
+ }
92
+ console.log(`Warmth: ${p.warmth.warmth} (${p.warmth.attempts} ${p.warmth.attempts === 1 ? 'attempt' : 'attempts'})`);
93
+ console.log(`Next follow-up: ${p.prospecting?.nextFollowUpAt ?? 'Not scheduled'}`);
94
+ console.log('Notes:');
95
+ if (p.notes.length === 0)
96
+ console.log(' (none yet)');
97
+ for (const note of [...p.notes].reverse()) {
98
+ console.log(` [${note.createdAt}]${note.outcome ? ` (${note.outcome})` : ''} ${note.body}`);
99
+ }
100
+ }));
101
+ prospect
102
+ .command('pipeline')
103
+ .description('The funnel\'s shape — counts by warmth, and how many prospects are still workable')
104
+ .option('--json', 'Output as JSON')
105
+ .action(runAction(async (opts) => {
106
+ const summary = await requireClient().prospectPipeline();
107
+ if (opts.json) {
108
+ printResult(summary, true);
109
+ return;
110
+ }
111
+ console.log(`${summary.workable} workable of ${summary.total} prospects`);
112
+ for (const warmth of ['hot', 'warm', 'untouched', 'cold', 'stalled']) {
113
+ const count = summary.counts[warmth];
114
+ if (count > 0)
115
+ console.log(` ${warmth.padEnd(10)} ${count}`);
116
+ }
117
+ // The actionable read, rather than leaving it to be inferred from the
118
+ // numbers: stalled is the only bucket that is genuinely spent.
119
+ if (summary.workable === 0 && summary.total > 0) {
120
+ console.log('\nEvery prospect has stalled — time to find more leads.');
121
+ }
122
+ }));
123
+ prospect
124
+ .command('create')
125
+ .description('Create a new prospect')
126
+ .argument('<name>')
127
+ .option('--status <status>', 'prospect, active, churned, or lost', 'prospect')
128
+ .option('--contact-name <name>')
129
+ .option('--email <email>')
130
+ .option('--phone <phone>')
131
+ .option('--note <text>', 'A seed note — e.g. how/where this prospect was found')
132
+ .option('--json', 'Output as JSON')
133
+ .action(runAction(async (name, opts) => {
134
+ if (!isValidStatus(opts.status)) {
135
+ console.error(`--status must be one of: ${STATUSES.join(', ')}`);
136
+ process.exitCode = 1;
137
+ return;
138
+ }
139
+ const hasContact = opts.contactName || opts.email || opts.phone;
140
+ const result = await requireClient().createProspect({
141
+ name,
142
+ status: opts.status,
143
+ primaryContact: hasContact
144
+ ? { name: opts.contactName ?? null, email: opts.email ?? null, phone: opts.phone ?? null }
145
+ : null,
146
+ note: opts.note,
147
+ });
148
+ printResult(result, opts.json);
149
+ }));
150
+ prospect
151
+ .command('update')
152
+ .description("Update a prospect's name or contact info")
153
+ .argument('<id>')
154
+ .option('--name <name>')
155
+ .option('--contact-name <name>')
156
+ .option('--email <email>')
157
+ .option('--phone <phone>')
158
+ .option('--json', 'Output as JSON')
159
+ .action(runAction(async (id, opts) => {
160
+ const hasContact = opts.contactName !== undefined || opts.email !== undefined || opts.phone !== undefined;
161
+ if (opts.name === undefined && !hasContact) {
162
+ console.error('Provide at least one field to update');
163
+ process.exitCode = 1;
164
+ return;
165
+ }
166
+ const result = await requireClient().updateProspect(id, {
167
+ name: opts.name,
168
+ primaryContact: hasContact
169
+ ? { name: opts.contactName ?? null, email: opts.email ?? null, phone: opts.phone ?? null }
170
+ : undefined,
171
+ });
172
+ printResult(result, opts.json);
173
+ }));
174
+ prospect
175
+ .command('log')
176
+ .description('Log a call — a note, and optionally a next follow-up date, together')
177
+ .argument('<id>')
178
+ .requiredOption('--note <text>', 'What happened')
179
+ .option('--outcome <outcome>', `how far it got — one of: ${CALL_OUTCOMES.join(', ')}`)
180
+ .option('--next <date>', 'ISO date (or anything Date can parse) for the next follow-up')
181
+ .option('--clear-next', 'Clear any scheduled follow-up')
182
+ .option('--json', 'Output as JSON')
183
+ .action(runAction(async (id, opts) => {
184
+ const followUp = resolveFollowUpUpdate(opts);
185
+ if (!followUp.ok) {
186
+ console.error(followUp.error);
187
+ process.exitCode = 1;
188
+ return;
189
+ }
190
+ if (opts.outcome !== undefined && !isValidOutcome(opts.outcome)) {
191
+ console.error(`--outcome must be one of: ${CALL_OUTCOMES.join(', ')}`);
192
+ process.exitCode = 1;
193
+ return;
194
+ }
195
+ const result = await requireClient().logProspectCall(id, {
196
+ note: opts.note,
197
+ nextFollowUpAt: followUp.value,
198
+ // Omitted means "not a contact attempt", which is what keeps a
199
+ // research note out of the attempt tally that decides stalling.
200
+ outcome: opts.outcome ?? null,
201
+ });
202
+ printResult(result, opts.json);
203
+ }));
204
+ prospect
205
+ .command('status')
206
+ .description('Move a prospect through prospect → active/lost (or a client through active ↔ churned)')
207
+ .argument('<id>')
208
+ .argument('<status>', 'prospect, active, churned, or lost')
209
+ .option('--reason <reason>', 'Carried with a lost or churned move')
210
+ .option('--json', 'Output as JSON')
211
+ .action(runAction(async (id, status, opts) => {
212
+ if (!isValidStatus(status)) {
213
+ console.error(`status must be one of: ${STATUSES.join(', ')}`);
214
+ process.exitCode = 1;
215
+ return;
216
+ }
217
+ const result = await requireClient().setProspectStatus(id, { status, reason: opts.reason });
218
+ printResult(result, opts.json);
219
+ }));
220
+ }
221
+ export { isValidOutcome, isValidSort, isValidStatus, resolveFollowUpUpdate };
222
+ //# sourceMappingURL=prospect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prospect.js","sourceRoot":"","sources":["../../src/commands/prospect.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ import { Command } from 'commander';
2
+ declare function isValidRequestStatus(status: string): boolean;
3
+ declare function isValidRequestType(type: string): boolean;
4
+ /**
5
+ * Every status name is phrased from the **Client's** side, because that is who
6
+ * reads it on their own dashboard — so the one status an agency user is most
7
+ * likely to misread is `waiting-on-you`, which means waiting on *them*, not on
8
+ * you. Printed alongside the raw name rather than instead of it: the raw name
9
+ * is what goes back into `request status`.
10
+ */
11
+ declare const STAFF_LABELS: Record<string, string>;
12
+ type Assignment = {
13
+ ok: true;
14
+ value: 'any' | 'unassigned' | 'mine';
15
+ } | {
16
+ ok: false;
17
+ error: string;
18
+ };
19
+ /** `--mine` and `--unassigned` ask opposite questions, so passing both is a mistake rather than a narrowing. */
20
+ declare function resolveAssignment(opts: {
21
+ mine?: boolean;
22
+ unassigned?: boolean;
23
+ }): Assignment;
24
+ /**
25
+ * RFC 0005 — the Request queue, as the CLI sees it.
26
+ *
27
+ * A Request is a Client asking the Agency for work, so unlike every other
28
+ * command group here nothing about it lives in a Site's repo. The whole surface
29
+ * is staff-only for that reason: a thread carries internal notes, and claiming
30
+ * one only means something from inside the Agency.
31
+ *
32
+ * There is deliberately no `create` — a Request comes from the Client, through
33
+ * their own dashboard. Filing one on their behalf would put words in their mouth
34
+ * and make the queue's own numbers a measure of us rather than of them.
35
+ */
36
+ export declare function registerRequestCommands(program: Command): void;
37
+ export { isValidRequestStatus, isValidRequestType, resolveAssignment, STAFF_LABELS };
38
+ //# sourceMappingURL=request.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request.d.ts","sourceRoot":"","sources":["../../src/commands/request.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAIpC,iBAAS,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAErD;AAED,iBAAS,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEjD;AAED;;;;;;GAMG;AACH,QAAA,MAAM,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAMxC,CAAC;AAEF,KAAK,UAAU,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,KAAK,GAAG,YAAY,GAAG,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpG,gHAAgH;AAChH,iBAAS,iBAAiB,CAAC,IAAI,EAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,UAAU,CAOrF;AA+BD;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAwK9D;AAED,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,YAAY,EAAE,CAAC"}
@@ -0,0 +1,214 @@
1
+ import { REQUEST_STATUSES, REQUEST_TYPES } from '../lib/client.js';
2
+ import { printResult, requireClient, runAction } from '../lib/cli-utils.js';
3
+ function isValidRequestStatus(status) {
4
+ return REQUEST_STATUSES.includes(status);
5
+ }
6
+ function isValidRequestType(type) {
7
+ return REQUEST_TYPES.includes(type);
8
+ }
9
+ /**
10
+ * Every status name is phrased from the **Client's** side, because that is who
11
+ * reads it on their own dashboard — so the one status an agency user is most
12
+ * likely to misread is `waiting-on-you`, which means waiting on *them*, not on
13
+ * you. Printed alongside the raw name rather than instead of it: the raw name
14
+ * is what goes back into `request status`.
15
+ */
16
+ const STAFF_LABELS = {
17
+ open: 'Open — nobody has picked it up',
18
+ 'in-progress': 'In progress',
19
+ 'waiting-on-you': 'Waiting on client',
20
+ done: 'Done',
21
+ cancelled: 'Cancelled',
22
+ };
23
+ /** `--mine` and `--unassigned` ask opposite questions, so passing both is a mistake rather than a narrowing. */
24
+ function resolveAssignment(opts) {
25
+ if (opts.mine && opts.unassigned) {
26
+ return { ok: false, error: '--mine and --unassigned are mutually exclusive' };
27
+ }
28
+ if (opts.mine)
29
+ return { ok: true, value: 'mine' };
30
+ if (opts.unassigned)
31
+ return { ok: true, value: 'unassigned' };
32
+ return { ok: true, value: 'any' };
33
+ }
34
+ function authorOf(thread, message) {
35
+ if (!message.authorUserId)
36
+ return 'Scribe';
37
+ return thread.actors[message.authorUserId]?.name ?? 'Someone';
38
+ }
39
+ function printThread(thread) {
40
+ const { request } = thread;
41
+ console.log(`${request.subject}`);
42
+ console.log(`Site: ${thread.site ?? request.siteId} Type: ${request.type}`);
43
+ console.log(`Status: ${request.status} (${STAFF_LABELS[request.status] ?? request.status})`);
44
+ console.log(`Assigned: ${request.assignedToUserId ? (thread.actors[request.assignedToUserId]?.name ?? 'someone') : 'nobody'}`);
45
+ if (request.page)
46
+ console.log(`Page: ${request.page}`);
47
+ console.log(`Opened: ${request.createdAt} Last activity: ${request.lastActivityAt}`);
48
+ console.log('');
49
+ console.log(request.body);
50
+ console.log('');
51
+ console.log('Thread:');
52
+ if (thread.messages.length === 0) {
53
+ console.log(' (nothing yet — the request body above is the whole of it)');
54
+ return;
55
+ }
56
+ for (const message of thread.messages) {
57
+ // The internal marker is first, not a suffix: it decides whether the line
58
+ // can be quoted back to the Client at all.
59
+ const tags = [message.internal ? 'INTERNAL' : null, message.kind === 'system' ? 'system' : null].filter(Boolean).join(' ');
60
+ console.log(` [${message.createdAt}] ${tags ? `(${tags}) ` : ''}${authorOf(thread, message)}: ${message.body}`);
61
+ }
62
+ }
63
+ /**
64
+ * RFC 0005 — the Request queue, as the CLI sees it.
65
+ *
66
+ * A Request is a Client asking the Agency for work, so unlike every other
67
+ * command group here nothing about it lives in a Site's repo. The whole surface
68
+ * is staff-only for that reason: a thread carries internal notes, and claiming
69
+ * one only means something from inside the Agency.
70
+ *
71
+ * There is deliberately no `create` — a Request comes from the Client, through
72
+ * their own dashboard. Filing one on their behalf would put words in their mouth
73
+ * and make the queue's own numbers a measure of us rather than of them.
74
+ */
75
+ export function registerRequestCommands(program) {
76
+ const request = program.command('request').description('Work the queue of requests clients have sent in');
77
+ request
78
+ .command('list')
79
+ .description('List requests, oldest activity first — the queue, worked from the end that has waited longest')
80
+ .option('--site <slug>', 'Only this site\'s requests')
81
+ .option('--status <status>', `one of: ${REQUEST_STATUSES.join(', ')}`)
82
+ .option('--type <type>', `one of: ${REQUEST_TYPES.join(', ')}`)
83
+ .option('--mine', 'Only requests this key\'s owner has claimed')
84
+ .option('--unassigned', 'Only requests nobody has claimed yet')
85
+ .option('--all', 'Include finished requests (done, cancelled), which are hidden by default')
86
+ .option('--sort <sort>', 'oldest (default) or newest, by last activity')
87
+ .option('--page <n>', 'Page number, 1-based')
88
+ .option('--json', 'Output as JSON')
89
+ .action(runAction(async (opts) => {
90
+ if (opts.status && !isValidRequestStatus(opts.status)) {
91
+ console.error(`--status must be one of: ${REQUEST_STATUSES.join(', ')}`);
92
+ process.exitCode = 1;
93
+ return;
94
+ }
95
+ if (opts.type && !isValidRequestType(opts.type)) {
96
+ console.error(`--type must be one of: ${REQUEST_TYPES.join(', ')}`);
97
+ process.exitCode = 1;
98
+ return;
99
+ }
100
+ if (opts.sort && opts.sort !== 'newest' && opts.sort !== 'oldest') {
101
+ console.error('--sort must be newest or oldest');
102
+ process.exitCode = 1;
103
+ return;
104
+ }
105
+ const assignment = resolveAssignment(opts);
106
+ if (!assignment.ok) {
107
+ console.error(assignment.error);
108
+ process.exitCode = 1;
109
+ return;
110
+ }
111
+ const result = await requireClient().listRequests({
112
+ site: opts.site,
113
+ status: opts.status,
114
+ type: opts.type,
115
+ assignment: assignment.value,
116
+ includeClosed: opts.all,
117
+ sort: opts.sort,
118
+ page: opts.page ? Number(opts.page) : undefined,
119
+ });
120
+ if (opts.json) {
121
+ printResult(result, true);
122
+ return;
123
+ }
124
+ // An empty queue is the most common answer there is, and
125
+ // `console.table([])` renders it as an empty frame that reads like a
126
+ // broken command rather than good news.
127
+ if (result.items.length === 0) {
128
+ console.log(opts.all || opts.status ? 'No requests match.' : 'Nothing in the queue.');
129
+ return;
130
+ }
131
+ printResult(result.items.map((item) => ({
132
+ id: item.id,
133
+ site: item.site ?? '',
134
+ subject: item.subject,
135
+ type: item.type,
136
+ status: item.status,
137
+ claimed: item.assignedToUserId ? 'yes' : 'no',
138
+ lastActivity: item.lastActivityAt,
139
+ })), false);
140
+ // The total is the only way to know there is a page 2, and a queue
141
+ // that silently showed its first 25 of 60 is the way one stops being
142
+ // believed.
143
+ if (result.total > result.items.length) {
144
+ console.log(`\nShowing ${result.items.length} of ${result.total} — pass --page 2 for more.`);
145
+ }
146
+ }));
147
+ request
148
+ .command('show')
149
+ .description('Show one request: the ask, its status, and the whole thread including internal notes')
150
+ .argument('<id>')
151
+ .option('--json', 'Output as JSON')
152
+ .action(runAction(async (id, opts) => {
153
+ const thread = await requireClient().getRequest(id);
154
+ if (opts.json) {
155
+ printResult(thread, true);
156
+ return;
157
+ }
158
+ printThread(thread);
159
+ }));
160
+ request
161
+ .command('claim')
162
+ .description("Claim a request for this key's owner, or release it with --release")
163
+ .argument('<id>')
164
+ .option('--release', 'Hand it back to nobody instead of claiming it')
165
+ .option('--json', 'Output as JSON')
166
+ .action(runAction(async (id, opts) => {
167
+ const thread = await requireClient().updateRequest(id, { assignTo: opts.release ? null : 'me' });
168
+ if (opts.json) {
169
+ printResult(thread, true);
170
+ return;
171
+ }
172
+ console.log(opts.release ? `Released: ${thread.request.subject}` : `Claimed: ${thread.request.subject}`);
173
+ }));
174
+ request
175
+ .command('reply')
176
+ .description('Reply to the client on a request, or add an Agency-only note with --internal')
177
+ .argument('<id>')
178
+ .requiredOption('--message <text>', 'What to say')
179
+ .option('--internal', 'An internal note: never shown to the client, and emails nobody')
180
+ .option('--json', 'Output as JSON')
181
+ .action(runAction(async (id, opts) => {
182
+ const thread = await requireClient().replyToRequest(id, { body: opts.message, internal: opts.internal });
183
+ if (opts.json) {
184
+ printResult(thread, true);
185
+ return;
186
+ }
187
+ console.log(opts.internal ? 'Note added (internal — the client will not see it).' : 'Replied — the client has been emailed.');
188
+ // A visible reply on an `open` request also picks it up, which is a
189
+ // status change the caller didn't ask for and should hear about.
190
+ if (thread.movedTo)
191
+ console.log(`Status is now: ${thread.movedTo}`);
192
+ }));
193
+ request
194
+ .command('status')
195
+ .description('Move a request to another status')
196
+ .argument('<id>')
197
+ .argument('<status>', REQUEST_STATUSES.join(', '))
198
+ .option('--json', 'Output as JSON')
199
+ .action(runAction(async (id, status, opts) => {
200
+ if (!isValidRequestStatus(status)) {
201
+ console.error(`status must be one of: ${REQUEST_STATUSES.join(', ')}`);
202
+ process.exitCode = 1;
203
+ return;
204
+ }
205
+ const thread = await requireClient().updateRequest(id, { status });
206
+ if (opts.json) {
207
+ printResult(thread, true);
208
+ return;
209
+ }
210
+ console.log(`${thread.request.subject} is now ${thread.request.status} (${STAFF_LABELS[thread.request.status] ?? ''}).`);
211
+ }));
212
+ }
213
+ export { isValidRequestStatus, isValidRequestType, resolveAssignment, STAFF_LABELS };
214
+ //# sourceMappingURL=request.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request.js","sourceRoot":"","sources":["../../src/commands/request.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,14 @@
1
1
  import { Command } from 'commander';
2
+ /**
3
+ * True when changing a Site's stored domain to `next` would be a **cutover**
4
+ * rather than a correction — i.e. the Site is still on its provisional
5
+ * `<owner>.github.io` Pages host and is being moved to a real hostname.
6
+ *
7
+ * `site update --domain` only rewrites the Mongo record and the Turnstile
8
+ * widget. It does not touch DNS, `public/CNAME`, or the repo's Pages settings,
9
+ * so using it for a cutover leaves the Site claiming a domain that serves
10
+ * nothing — the half-state `site promote` exists to prevent.
11
+ */
12
+ export declare function isDomainCutover(current: string, next: string, githubOwner: string): boolean;
2
13
  export declare function registerSiteCommands(program: Command): void;
3
14
  //# sourceMappingURL=site.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"site.d.ts","sourceRoot":"","sources":["../../src/commands/site.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA0CpC,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA0V3D"}
1
+ {"version":3,"file":"site.d.ts","sourceRoot":"","sources":["../../src/commands/site.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA0CpC;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAG3F;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAwX3D"}
@@ -35,6 +35,20 @@ function printDnsRecords(records, write = console.error) {
35
35
  }
36
36
  write('In Cloudflare the record must be DNS-only (grey cloud), not proxied.\n');
37
37
  }
38
+ /**
39
+ * True when changing a Site's stored domain to `next` would be a **cutover**
40
+ * rather than a correction — i.e. the Site is still on its provisional
41
+ * `<owner>.github.io` Pages host and is being moved to a real hostname.
42
+ *
43
+ * `site update --domain` only rewrites the Mongo record and the Turnstile
44
+ * widget. It does not touch DNS, `public/CNAME`, or the repo's Pages settings,
45
+ * so using it for a cutover leaves the Site claiming a domain that serves
46
+ * nothing — the half-state `site promote` exists to prevent.
47
+ */
48
+ export function isDomainCutover(current, next, githubOwner) {
49
+ const pagesHost = `${githubOwner}.github.io`.toLowerCase();
50
+ return current.toLowerCase() === pagesHost && next.toLowerCase() !== pagesHost;
51
+ }
38
52
  export function registerSiteCommands(program) {
39
53
  const site = program.command('site').description('Manage sites');
40
54
  site
@@ -51,7 +65,7 @@ export function registerSiteCommands(program) {
51
65
  .command('add')
52
66
  .description('Provision a new site: create its GitHub repo, push a starter template, and register it')
53
67
  .requiredOption('--slug <slug>', 'Site slug (also used as the repo name)')
54
- .option('--domain <domain>', 'Site domain (defaults to the GitHub Pages host — set the real one later with `scribe site update --domain`)')
68
+ .option('--domain <domain>', 'Site domain (defaults to the GitHub Pages host — take the site live on the real one later with `scribe site promote`)')
55
69
  .option('--turnstile-key <key>', 'Cloudflare Turnstile site key (defaults to the server-configured shared widget)')
56
70
  .option('--name <name>', 'Display name (defaults to the slug)')
57
71
  .option('--form-collection <name>', 'Name for the default form/inbox collection (defaults to "inquiries")')
@@ -71,10 +85,11 @@ export function registerSiteCommands(program) {
71
85
  }));
72
86
  site
73
87
  .command('update')
74
- .description("Update a site's settings: its domain and/or Turnstile site key")
88
+ .description("Correct a site's stored settings: its domain and/or Turnstile site key")
75
89
  .argument('<slug>', 'Site slug')
76
90
  .option('--turnstile-key <key>', 'Cloudflare Turnstile site key')
77
- .option('--domain <domain>', "The site's public hostname, e.g. example.com — also synced to the Turnstile widget")
91
+ .option('--domain <domain>', "Correct the site's stored hostname (record + Turnstile only). To take a site live on a new domain use `scribe site promote`")
92
+ .option('--record-only', 'Allow --domain to move a site off its github.io host without doing the cutover')
78
93
  .action(runAction(async (slug, opts) => {
79
94
  if (!opts.turnstileKey && !opts.domain) {
80
95
  console.error('Nothing to update — pass --turnstile-key and/or --domain.');
@@ -82,6 +97,29 @@ export function registerSiteCommands(program) {
82
97
  return;
83
98
  }
84
99
  const client = requireClient();
100
+ // A cutover reached through `update` is the classic half-move: the
101
+ // record names a domain that DNS, Pages and `public/CNAME` know
102
+ // nothing about, so the site 404s on the new host while still
103
+ // reporting healthy. Refuse rather than warn — a warning on stderr is
104
+ // exactly what gets scrolled past.
105
+ if (opts.domain && !opts.recordOnly) {
106
+ const existing = await client
107
+ .listSites()
108
+ .then((sites) => sites.find((s) => s.slug === slug))
109
+ // A lookup failure shouldn't block a legitimate correction.
110
+ .catch(() => undefined);
111
+ if (existing && isDomainCutover(existing.domain, opts.domain, existing.githubOwner)) {
112
+ console.error(`"${slug}" is still on ${existing.domain}, so this is a cutover, not a correction.`);
113
+ console.error('`site update --domain` changes only the stored record and the Turnstile widget —');
114
+ console.error('it does not touch DNS, public/CNAME, or the repo\'s GitHub Pages settings, so the');
115
+ console.error('site would 404 on the new domain. Use promote instead, which does all of it:\n');
116
+ console.error(` scribe site promote ${slug} --domain ${opts.domain} --dns-only # check DNS, change nothing`);
117
+ console.error(` scribe site promote ${slug} --domain ${opts.domain} # cut over\n`);
118
+ console.error('Pass --record-only if you really do just want to rewrite the stored value.');
119
+ process.exitCode = 1;
120
+ throw new CliExit();
121
+ }
122
+ }
85
123
  const result = await client.updateSite(slug, {
86
124
  turnstileSiteKey: opts.turnstileKey,
87
125
  domain: opts.domain,