crewly 1.20.453 → 1.20.455

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 (39) hide show
  1. package/config/skills/agent/checkup-end/SKILL.md +86 -0
  2. package/config/skills/agent/checkup-end/end.mjs +339 -0
  3. package/config/skills/agent/checkup-end/execute.sh +39 -0
  4. package/config/skills/agent/checkup-end/execute.test.sh +134 -0
  5. package/config/skills/agent/checkup-report/SKILL.md +8 -0
  6. package/config/skills/agent/ingest-exports/SKILL.md +2 -2
  7. package/config/templates/small-business-checkup.json +1 -1
  8. package/dist/backend/backend/src/constants.d.ts +8 -0
  9. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  10. package/dist/backend/backend/src/constants.js +8 -0
  11. package/dist/backend/backend/src/constants.js.map +1 -1
  12. package/dist/backend/backend/src/controllers/canva/canva.controller.d.ts.map +1 -1
  13. package/dist/backend/backend/src/controllers/canva/canva.controller.js +3 -0
  14. package/dist/backend/backend/src/controllers/canva/canva.controller.js.map +1 -1
  15. package/dist/backend/backend/src/controllers/connector/connector.controller.d.ts.map +1 -1
  16. package/dist/backend/backend/src/controllers/connector/connector.controller.js +9 -1
  17. package/dist/backend/backend/src/controllers/connector/connector.controller.js.map +1 -1
  18. package/dist/backend/backend/src/controllers/connector/remote-mcp-proxy.controller.d.ts.map +1 -1
  19. package/dist/backend/backend/src/controllers/connector/remote-mcp-proxy.controller.js +5 -0
  20. package/dist/backend/backend/src/controllers/connector/remote-mcp-proxy.controller.js.map +1 -1
  21. package/dist/backend/backend/src/controllers/connector/zoho-draft.controller.d.ts.map +1 -1
  22. package/dist/backend/backend/src/controllers/connector/zoho-draft.controller.js +4 -0
  23. package/dist/backend/backend/src/controllers/connector/zoho-draft.controller.js.map +1 -1
  24. package/dist/backend/backend/src/controllers/google/google.controller.d.ts.map +1 -1
  25. package/dist/backend/backend/src/controllers/google/google.controller.js +9 -0
  26. package/dist/backend/backend/src/controllers/google/google.controller.js.map +1 -1
  27. package/dist/backend/backend/src/controllers/microsoft/microsoft-todo.controller.d.ts.map +1 -1
  28. package/dist/backend/backend/src/controllers/microsoft/microsoft-todo.controller.js +3 -0
  29. package/dist/backend/backend/src/controllers/microsoft/microsoft-todo.controller.js.map +1 -1
  30. package/dist/backend/backend/src/services/connector/connector-use-audit.service.d.ts +100 -0
  31. package/dist/backend/backend/src/services/connector/connector-use-audit.service.d.ts.map +1 -0
  32. package/dist/backend/backend/src/services/connector/connector-use-audit.service.js +142 -0
  33. package/dist/backend/backend/src/services/connector/connector-use-audit.service.js.map +1 -0
  34. package/dist/backend/build-info.json +2 -2
  35. package/dist/cli/backend/src/constants.d.ts +8 -0
  36. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  37. package/dist/cli/backend/src/constants.js +8 -0
  38. package/dist/cli/backend/src/constants.js.map +1 -1
  39. package/package.json +1 -1
@@ -0,0 +1,86 @@
1
+ ---
2
+ name: Check-up End
3
+ description: End a small-business check-up. Deletes every file the check-up produced on this machine (ingested exports, the analysis with its list of people, the report and daily documents, the intake answers, the time log, the App page copy, and optionally the raw CSV exports) and writes a receipt the owner can keep that says what was deleted, what was kept and what is still to do (unpublish the App, revoke any access). --dry-run lists without deleting.
4
+ version: 1.0.0
5
+ category: productivity
6
+ skillType: claude-skill
7
+ assignableRoles:
8
+ - "*"
9
+ triggers:
10
+ - end the check-up
11
+ - check-up is over
12
+ - delete check-up data
13
+ - wipe the check-up
14
+ - check-up receipt
15
+ tags:
16
+ - check-up
17
+ - small-business
18
+ - privacy
19
+ - cleanup
20
+ execution:
21
+ type: script
22
+ script:
23
+ file: execute.sh
24
+ interpreter: bash
25
+ timeoutMs: 60000
26
+ ---
27
+
28
+ # Check-up End
29
+
30
+ The last step of the small-business check-up (crewly#1055). The check-up
31
+ leaves rows of people on this machine: the ingested exports, `breaks.json`
32
+ with every person behind every break, the report document, the intake
33
+ answers. When the check-up is over (the 7 daily lines are posted, or the
34
+ owner says stop), delete all of it and give the owner a receipt.
35
+
36
+ ```bash
37
+ bash execute.sh --dir . --business "Oak Pottery" --app <appId> # from the project the team worked in
38
+ bash execute.sh --dir . --exports ~/Downloads/oak-exports # also the raw CSVs the owner handed over
39
+ bash execute.sh --dir . --dry-run # list what would go, delete nothing
40
+ ```
41
+
42
+ ## What it deletes
43
+
44
+ Only the files the check-up skills are known to write, in the folders the
45
+ team template uses under `--dir` (override any with a flag):
46
+
47
+ | Folder / file | Flag | Files |
48
+ |---|---|---|
49
+ | `ingest/` | `--ingest` | `*.json` from `ingest-exports` (rows of people), `summary.json`, `unknown-report.json` |
50
+ | `analysis/` | `--analysis` | `breaks.json`, `report.md` from `checkup-analyse` |
51
+ | `checkup-data/` (or `data/`) | `--data` | `report-doc.json`, `daily-YYYY-MM-DD.json`, `timelog.json` from `checkup-report` |
52
+ | `checkup-app/` | `--app-dir` | `index.html` (the App page copy) |
53
+ | `intake.json` | `--intake` | the answers read with `app-data --get intake answers` |
54
+ | raw exports | `--exports <dir>` | `*.csv` in that folder, not recursive; only when you pass it |
55
+
56
+ Anything else in those folders (a stray file, a sub-folder, a symbolic link)
57
+ is **left alone and listed in the receipt as a leftover**. A folder that ends
58
+ up empty is removed. If none of `ingest/summary.json`, `analysis/breaks.json`,
59
+ `report-doc.json`, `timelog.json` or `intake.json` exists the skill refuses
60
+ (exit 2) and deletes nothing, so it cannot be pointed at the wrong folder.
61
+
62
+ Run `--dry-run` first when you are not sure what is there; it writes the same
63
+ receipt marked as a dry run.
64
+
65
+ ## The receipt
66
+
67
+ `checkup-end-receipt.json` and `checkup-end-receipt.md` in `--dir` (or
68
+ `--receipt <dir>`): the business, when, every file deleted with its kind,
69
+ size and whether it held people data, whether names were hashed at ingest,
70
+ the leftovers, and **Still to do**. Send the owner the `.md` or say the same
71
+ in plain words: what was deleted, that nothing about their customers stays on
72
+ this machine, and what you are doing next.
73
+
74
+ ## Still to do — the two things this skill cannot do itself
75
+
76
+ 1. **Unpublish the App.** The report, the intake answers and the daily lines
77
+ live in the App until it is deleted. Run `publish-app --app <appId> --delete`
78
+ (pass `--app` here so the receipt carries the exact command). A public App
79
+ needs `--private` first.
80
+ 2. **Revoke access.** Crewly stored no key or login for the apps named in the
81
+ intake (this check-up runs on exports); if the owner handed you a key, a
82
+ login or a read-only share, tell them to revoke it in that app. The receipt
83
+ lists the apps from the intake answers.
84
+
85
+ Never delete before the owner has the report: the 7 daily lines come first,
86
+ or the owner's word that the check-up is over.
@@ -0,0 +1,339 @@
1
+ #!/usr/bin/env node
2
+ // =============================================================================
3
+ // checkup-end — the last step of the small-business check-up (crewly#1055):
4
+ // delete every file the check-up produced on this machine (ingested exports,
5
+ // the analysis with its list of people, the report and daily documents, the
6
+ // intake answers, the time log, the copied App page, and optionally the raw
7
+ // CSV exports the owner handed over) and write a receipt that says what was
8
+ // deleted, what was kept, and what the lead still has to do (unpublish the
9
+ // App, revoke any access the owner granted).
10
+ //
11
+ // node end.mjs --dir <project> [--business "Oak Pottery"] [--app <appId>] [--exports <csv dir>] [--dry-run]
12
+ // node end.mjs --dir . --ingest ./ingest --analysis ./analysis --data ./checkup-data --app-dir ./checkup-app --intake ./intake.json
13
+ //
14
+ // Only the files the check-up skills are known to write are deleted; anything
15
+ // else found in those folders is listed in the receipt as a leftover and left
16
+ // alone. Symbolic links are never followed. Exit 0 = done (or dry run),
17
+ // 2 = the folder does not look like a check-up, 1 = usage.
18
+ // =============================================================================
19
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, lstatSync, unlinkSync, rmdirSync } from 'node:fs';
20
+ import { join, resolve, relative, basename } from 'node:path';
21
+
22
+ /** Where the check-up team writes each kind of file, relative to the project (`--dir`). */
23
+ export const DEFAULT_LAYOUT = {
24
+ ingest: ['ingest'],
25
+ analysis: ['analysis'],
26
+ data: ['checkup-data', 'data'],
27
+ appDir: ['checkup-app'],
28
+ intake: ['intake.json'],
29
+ };
30
+
31
+ const RECEIPT_JSON = 'checkup-end-receipt.json';
32
+ const RECEIPT_MD = 'checkup-end-receipt.md';
33
+
34
+ /**
35
+ * What each check-up skill writes, by folder: file-name test and what the
36
+ * receipt calls it. Only these are deleted.
37
+ */
38
+ const KNOWN = {
39
+ ingest: [
40
+ { test: (f) => f === 'summary.json', kind: 'ingest summary', people: false },
41
+ { test: (f) => f === 'unknown-report.json', kind: 'ingest note (unmapped file)', people: true },
42
+ { test: (f) => f.endsWith('.json'), kind: 'ingested export (rows of people)', people: true },
43
+ ],
44
+ analysis: [
45
+ { test: (f) => f === 'breaks.json', kind: 'analysis (every break with its people)', people: true },
46
+ { test: (f) => f === 'report.md', kind: 'analysis report', people: true },
47
+ ],
48
+ data: [
49
+ { test: (f) => f === 'report-doc.json', kind: 'App report document', people: true },
50
+ { test: (f) => /^daily-\d{4}-\d{2}-\d{2}\.json$/.test(f), kind: 'daily summary', people: false },
51
+ { test: (f) => f === 'timelog.json', kind: 'time log', people: false },
52
+ ],
53
+ appDir: [
54
+ { test: (f) => f === 'index.html', kind: 'App page copy', people: false },
55
+ ],
56
+ exports: [
57
+ { test: (f) => /\.csv$/i.test(f), kind: 'raw export (owner-provided CSV)', people: true },
58
+ ],
59
+ };
60
+
61
+ /** Files that mark a folder as a check-up; at least one must exist or nothing is touched. */
62
+ const MARKERS = [
63
+ ['ingest', 'summary.json'],
64
+ ['analysis', 'breaks.json'],
65
+ ['data', 'report-doc.json'],
66
+ ['data', 'timelog.json'],
67
+ ['intake', null],
68
+ ];
69
+
70
+ /** Resolve a folder/file for one slot: the explicit flag, else the first default that exists under `--dir`. */
71
+ function resolveSlot(o, slot) {
72
+ if (o[slot]) return resolve(o[slot]);
73
+ for (const candidate of DEFAULT_LAYOUT[slot]) {
74
+ const p = resolve(o.dir, candidate);
75
+ if (existsSync(p)) return p;
76
+ }
77
+ return resolve(o.dir, DEFAULT_LAYOUT[slot][0]);
78
+ }
79
+
80
+ /** True when the path exists and is not a symbolic link (links are never followed). */
81
+ function isRealFile(p) {
82
+ try {
83
+ const st = lstatSync(p);
84
+ return st.isFile();
85
+ } catch {
86
+ return false;
87
+ }
88
+ }
89
+
90
+ function isRealDir(p) {
91
+ try {
92
+ return lstatSync(p).isDirectory();
93
+ } catch {
94
+ return false;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Walk one folder (not recursive) and split its entries into the files the
100
+ * check-up wrote (to delete) and everything else (to leave and report).
101
+ */
102
+ function inventoryDir(dir, rules, root) {
103
+ const toDelete = [];
104
+ const leftovers = [];
105
+ if (!isRealDir(dir)) return { toDelete, leftovers, present: false };
106
+ for (const name of readdirSync(dir).sort()) {
107
+ const p = join(dir, name);
108
+ const st = lstatSync(p);
109
+ const rel = relative(root, p) || name;
110
+ if (st.isSymbolicLink()) { leftovers.push({ path: rel, note: 'symbolic link, not followed' }); continue; }
111
+ if (st.isDirectory()) { leftovers.push({ path: rel, note: 'folder, not entered' }); continue; }
112
+ const rule = rules.find((r) => r.test(name));
113
+ if (rule) toDelete.push({ path: rel, abs: p, bytes: st.size, kind: rule.kind, people: rule.people });
114
+ else leftovers.push({ path: rel, note: 'not written by a check-up skill' });
115
+ }
116
+ return { toDelete, leftovers, present: true };
117
+ }
118
+
119
+ /** Read the intake answers (plain document or app-data's `{ success, data }`) for the apps to revoke; missing → null. */
120
+ function readIntakeApps(path) {
121
+ if (!isRealFile(path)) return null;
122
+ try {
123
+ const raw = JSON.parse(readFileSync(path, 'utf8'));
124
+ const doc = raw && raw.data && typeof raw.data === 'object' && !Array.isArray(raw.data) ? raw.data : raw;
125
+ return Array.isArray(doc.apps) ? doc.apps.filter((a) => typeof a === 'string' && a.trim()).map((a) => a.trim()) : [];
126
+ } catch {
127
+ return [];
128
+ }
129
+ }
130
+
131
+ /** Whether ingest-exports hashed the names (then the stored rows held no name, email or phone). */
132
+ function readNamesHashed(ingestDir) {
133
+ const p = join(ingestDir, 'summary.json');
134
+ if (!isRealFile(p)) return null;
135
+ try {
136
+ return Boolean(JSON.parse(readFileSync(p, 'utf8')).namesHashed);
137
+ } catch {
138
+ return null;
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Build the end-of-check-up plan: every file to delete, the leftovers, the
144
+ * App and access the lead still has to deal with. Deletes nothing.
145
+ */
146
+ export function planEnd(o) {
147
+ const root = resolve(o.dir);
148
+ const slots = {
149
+ ingest: resolveSlot(o, 'ingest'),
150
+ analysis: resolveSlot(o, 'analysis'),
151
+ data: resolveSlot(o, 'data'),
152
+ appDir: resolveSlot(o, 'appDir'),
153
+ intake: resolveSlot(o, 'intake'),
154
+ };
155
+ const looksLikeCheckup = MARKERS.some(([slot, file]) => (file ? isRealFile(join(slots[slot], file)) : isRealFile(slots[slot])));
156
+ const toDelete = [];
157
+ const leftovers = [];
158
+ const folders = [];
159
+ for (const slot of ['ingest', 'analysis', 'data', 'appDir']) {
160
+ const inv = inventoryDir(slots[slot], KNOWN[slot], root);
161
+ toDelete.push(...inv.toDelete);
162
+ leftovers.push(...inv.leftovers);
163
+ if (inv.present) folders.push(slots[slot]);
164
+ }
165
+ if (o.exports) {
166
+ const dir = resolve(o.exports);
167
+ const inv = inventoryDir(dir, KNOWN.exports, root);
168
+ toDelete.push(...inv.toDelete);
169
+ leftovers.push(...inv.leftovers);
170
+ if (inv.present) folders.push(dir);
171
+ }
172
+ if (isRealFile(slots.intake)) {
173
+ toDelete.push({ path: relative(root, slots.intake) || basename(slots.intake), abs: slots.intake, bytes: lstatSync(slots.intake).size, kind: 'intake answers', people: false });
174
+ }
175
+ const apps = readIntakeApps(slots.intake);
176
+ const namesHashed = readNamesHashed(slots.ingest);
177
+ return { root, slots, looksLikeCheckup, toDelete, leftovers, folders, apps, namesHashed };
178
+ }
179
+
180
+ /** The steps this skill cannot do itself, in the order the lead should take them. */
181
+ function todoFor(o, plan) {
182
+ const todo = [];
183
+ if (o.app) {
184
+ todo.push({
185
+ step: 'unpublish the App',
186
+ why: 'the report, the intake answers and the daily lines live in the App until it is deleted',
187
+ command: `publish-app --app ${o.app} --delete`,
188
+ });
189
+ } else {
190
+ todo.push({
191
+ step: 'unpublish the App',
192
+ why: 'the report, the intake answers and the daily lines live in the App until it is deleted (pass --app <appId> to put the exact command here)',
193
+ command: 'publish-app --app <appId> --delete',
194
+ });
195
+ }
196
+ const apps = plan.apps && plan.apps.length ? plan.apps : [];
197
+ todo.push({
198
+ step: 'revoke access',
199
+ why: apps.length
200
+ ? `the owner named ${apps.join(', ')}; Crewly stored no key or login for them in this check-up (exports only), so any key, login or read-only share the owner handed over is revoked in that app`
201
+ : 'any key, login or read-only share the owner handed over for the check-up is revoked in that app; Crewly stored none',
202
+ apps,
203
+ });
204
+ return todo;
205
+ }
206
+
207
+ /** The receipt as Markdown the lead can paste to the owner. */
208
+ function receiptMarkdown(r) {
209
+ const lines = [];
210
+ lines.push(`# Check-up ended${r.business ? ` — ${r.business}` : ''}`);
211
+ lines.push('');
212
+ lines.push(`Ended at ${r.endedAt}${r.dryRun ? ' (dry run: nothing was deleted)' : ''}.`);
213
+ lines.push('');
214
+ lines.push(`## Deleted (${r.deleted.length} file${r.deleted.length === 1 ? '' : 's'}, ${r.bytes.toLocaleString('en-US')} bytes)`);
215
+ lines.push('');
216
+ if (!r.deleted.length) lines.push('- nothing: no check-up files were found');
217
+ for (const f of r.deleted) lines.push(`- \`${f.path}\` — ${f.kind}${f.people ? ' (held people data)' : ''}`);
218
+ lines.push('');
219
+ if (r.namesHashed === true) lines.push('Names, emails and phones were hashed when the exports were ingested; the stored rows held none.');
220
+ else if (r.namesHashed === false) lines.push('The ingested rows held names as exported; they are gone with the files above.');
221
+ lines.push('');
222
+ lines.push('## Kept');
223
+ lines.push('');
224
+ lines.push(`- this receipt (\`${r.receipt.json}\`, \`${r.receipt.md}\`)`);
225
+ for (const l of r.leftovers) lines.push(`- \`${l.path}\` — ${l.note}`);
226
+ lines.push('');
227
+ lines.push('## Still to do');
228
+ lines.push('');
229
+ for (const t of r.todo) {
230
+ lines.push(`- **${t.step}**: ${t.why}${t.command ? ` — \`${t.command}\`` : ''}`);
231
+ }
232
+ lines.push('');
233
+ return lines.join('\n');
234
+ }
235
+
236
+ /** Delete the planned files, then remove any check-up folder left empty. Returns what was actually removed. */
237
+ function applyPlan(plan) {
238
+ const deleted = [];
239
+ const failed = [];
240
+ for (const f of plan.toDelete) {
241
+ try {
242
+ unlinkSync(f.abs);
243
+ deleted.push(f);
244
+ } catch (e) {
245
+ failed.push({ path: f.path, error: e.message });
246
+ }
247
+ }
248
+ const removedFolders = [];
249
+ for (const dir of plan.folders) {
250
+ try {
251
+ if (isRealDir(dir) && readdirSync(dir).length === 0) {
252
+ rmdirSync(dir);
253
+ removedFolders.push(relative(plan.root, dir) || dir);
254
+ }
255
+ } catch {
256
+ /* a folder that will not go is simply kept */
257
+ }
258
+ }
259
+ return { deleted, failed, removedFolders };
260
+ }
261
+
262
+ function out(obj, code) {
263
+ process.stdout.write(JSON.stringify(obj) + '\n');
264
+ process.exit(code);
265
+ }
266
+
267
+ function usage() {
268
+ out({ success: false, error: 'usage: end.mjs --dir <project> [--business <name>] [--app <appId>] [--exports <csv dir>] [--ingest <dir>] [--analysis <dir>] [--data <dir>] [--app-dir <dir>] [--intake <file>] [--receipt <dir>] [--dry-run]' }, 1);
269
+ }
270
+
271
+ function main(argv) {
272
+ const o = {};
273
+ for (let i = 0; i < argv.length; i += 1) {
274
+ const a = argv[i];
275
+ const next = () => { i += 1; if (i >= argv.length) usage(); return argv[i]; };
276
+ if (a === '--dir') o.dir = next();
277
+ else if (a === '--business') o.business = next();
278
+ else if (a === '--app') o.app = next();
279
+ else if (a === '--exports') o.exports = next();
280
+ else if (a === '--ingest') o.ingest = next();
281
+ else if (a === '--analysis') o.analysis = next();
282
+ else if (a === '--data') o.data = next();
283
+ else if (a === '--app-dir') o.appDir = next();
284
+ else if (a === '--intake') o.intake = next();
285
+ else if (a === '--receipt') o.receipt = next();
286
+ else if (a === '--dry-run') o.dryRun = true;
287
+ else usage();
288
+ }
289
+ if (!o.dir) usage();
290
+ if (!isRealDir(o.dir)) out({ success: false, error: `${o.dir}: not a folder` }, 2);
291
+ if (o.exports && !isRealDir(o.exports)) out({ success: false, error: `${o.exports}: not a folder` }, 2);
292
+
293
+ const plan = planEnd(o);
294
+ if (!plan.looksLikeCheckup) {
295
+ out({
296
+ success: false,
297
+ error: `${plan.root} does not look like a check-up folder (no ingest/summary.json, analysis/breaks.json, report-doc.json, timelog.json or intake.json); nothing deleted`,
298
+ }, 2);
299
+ }
300
+
301
+ const endedAt = new Date().toISOString();
302
+ const receiptDir = resolve(o.receipt || plan.root);
303
+ const receipt = { json: join(receiptDir, RECEIPT_JSON), md: join(receiptDir, RECEIPT_MD) };
304
+ const result = o.dryRun ? { deleted: plan.toDelete, failed: [], removedFolders: [] } : applyPlan(plan);
305
+ const bytes = result.deleted.reduce((n, f) => n + f.bytes, 0);
306
+ const r = {
307
+ schemaVersion: '1.0',
308
+ business: o.business || null,
309
+ endedAt,
310
+ dryRun: Boolean(o.dryRun),
311
+ root: plan.root,
312
+ deleted: result.deleted.map(({ path, bytes: b, kind, people }) => ({ path, bytes: b, kind, people })),
313
+ bytes,
314
+ failed: result.failed,
315
+ removedFolders: result.removedFolders,
316
+ leftovers: plan.leftovers,
317
+ namesHashed: plan.namesHashed,
318
+ todo: todoFor(o, plan),
319
+ receipt,
320
+ };
321
+ mkdirSync(receiptDir, { recursive: true });
322
+ writeFileSync(receipt.json, JSON.stringify(r, null, 2));
323
+ writeFileSync(receipt.md, receiptMarkdown(r));
324
+
325
+ out({
326
+ success: result.failed.length === 0,
327
+ dryRun: r.dryRun,
328
+ deleted: r.deleted.length,
329
+ bytes,
330
+ peopleFiles: r.deleted.filter((f) => f.people).length,
331
+ leftovers: r.leftovers.length,
332
+ failed: result.failed,
333
+ receipt,
334
+ next: r.todo.map((t) => t.command || `${t.step}: ${t.why}`),
335
+ }, result.failed.length ? 2 : 0);
336
+ }
337
+
338
+ const isMain = process.argv[1] && resolve(process.argv[1]) === resolve(new URL(import.meta.url).pathname);
339
+ if (isMain) main(process.argv.slice(2));
@@ -0,0 +1,39 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # checkup-end — end a small-business check-up (crewly#1055): delete every
4
+ # file the check-up produced on this machine and write a receipt that says
5
+ # what was deleted, what was kept and what the lead still has to do
6
+ # (unpublish the App, revoke any access the owner granted).
7
+ #
8
+ # Usage:
9
+ # bash execute.sh --dir . --business "Oak Pottery" --app <appId> # the project the team worked in
10
+ # bash execute.sh --dir . --exports ~/Downloads/oak-exports # also the raw CSVs the owner handed over
11
+ # bash execute.sh --dir . --dry-run # list what would go, delete nothing
12
+ # bash execute.sh '{"dir":".","business":"Oak Pottery","app":"28au74d9cj","dryRun":true}'
13
+ #
14
+ # Output: one JSON line (files deleted, bytes, leftovers, receipt paths, the
15
+ # commands still to run). Exit 0 = done, 2 = not a check-up folder or a file
16
+ # would not delete, 1 = usage.
17
+ # =============================================================================
18
+ set -euo pipefail
19
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
20
+ END="${SCRIPT_DIR}/end.mjs"
21
+
22
+ command -v node >/dev/null 2>&1 || { printf '{"success":false,"error":"node is required but not on PATH"}\n'; exit 1; }
23
+
24
+ ARGS=()
25
+ if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
26
+ command -v jq >/dev/null 2>&1 || { printf '{"success":false,"error":"jq is required to read JSON input"}\n'; exit 1; }
27
+ for key in dir business app exports ingest analysis data appDir intake receipt; do
28
+ v=$(printf '%s' "$1" | jq -r --arg k "$key" '.[$k] // empty')
29
+ if [ -n "$v" ]; then
30
+ case "$key" in appDir) flag=--app-dir ;; *) flag="--$key" ;; esac
31
+ ARGS+=("$flag" "$v")
32
+ fi
33
+ done
34
+ [ "$(printf '%s' "$1" | jq -r '.dryRun // false')" = "true" ] && ARGS+=(--dry-run)
35
+ shift
36
+ fi
37
+ ARGS+=("$@")
38
+
39
+ exec node "$END" ${ARGS[@]+"${ARGS[@]}"}
@@ -0,0 +1,134 @@
1
+ #!/bin/bash
2
+ # Tests for checkup-end — run with: bash execute.test.sh
3
+ # Offline: builds a check-up folder with ingest-exports + checkup-analyse + checkup-report first. Needs node and jq.
4
+ set -u
5
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+ EXEC="$HERE/execute.sh"
7
+ INGEST="$HERE/../ingest-exports/execute.sh"
8
+ ANALYSE="$HERE/../checkup-analyse/execute.sh"
9
+ REPORT="$HERE/../checkup-report/execute.sh"
10
+ FX="$HERE/../ingest-exports/fixtures"
11
+ PASS=0; FAIL=0
12
+ check() {
13
+ local name="$1"; local got="$2"; local want="$3"
14
+ if [ "$got" = "$want" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: $name"; echo " want: $want"; echo " got: $got"; fi
15
+ }
16
+ T="$(mktemp -d)"; trap 'rm -rf "$T"' EXIT
17
+
18
+ # A check-up laid out the way the team template works: project P with ingest/, analysis/, checkup-data/, checkup-app/, intake.json
19
+ build_checkup() {
20
+ local P="$1"; local hash="${2:-}"
21
+ mkdir -p "$P"
22
+ if [ -n "$hash" ]; then bash "$INGEST" --in "$FX" --out "$P/ingest" --hash-names >/dev/null; else bash "$INGEST" --in "$FX" --out "$P/ingest" >/dev/null; fi
23
+ bash "$ANALYSE" --in "$P/ingest" --as-of 2026-10-26 --out "$P/analysis" >/dev/null
24
+ cat > "$P/intake.json" <<'EOF'
25
+ {"apps":["Square","Jotform","Jackrabbit"],"slowestTask":"copying sign-ups","fears":[],"appsPerCustomer":"3","firstNumber":"Money owed to me","savedAt":"2026-10-26T10:00:00.000Z"}
26
+ EOF
27
+ bash "$REPORT" --log-step intake --start --out "$P/checkup-data" >/dev/null
28
+ bash "$REPORT" --log-step intake --end --out "$P/checkup-data" >/dev/null
29
+ bash "$REPORT" --breaks "$P/analysis/breaks.json" --business "Oak Pottery" --intake "$P/intake.json" --out "$P/checkup-data" >/dev/null
30
+ bash "$REPORT" --daily "Replied to 3 inquiries." --date 2026-10-27 --out "$P/checkup-data" >/dev/null
31
+ bash "$REPORT" --app-dir "$P/checkup-app" >/dev/null
32
+ }
33
+
34
+ # 1. Dry run: lists everything, deletes nothing, receipt marked dryRun
35
+ build_checkup "$T/p1"
36
+ BEFORE=$(find "$T/p1" -type f | wc -l | tr -d ' ')
37
+ OUT="$(bash "$EXEC" --dir "$T/p1" --business "Oak Pottery" --app 28au74d9cj --dry-run)"; RC=$?
38
+ check "dry run exits 0" "$RC" "0"
39
+ check "dry run success" "$(printf '%s' "$OUT" | jq -r '.success')" "true"
40
+ check "dry run flagged" "$(printf '%s' "$OUT" | jq -r '.dryRun')" "true"
41
+ check "dry run counts the files" "$(printf '%s' "$OUT" | jq -r '.deleted >= 12')" "true"
42
+ AFTER=$(find "$T/p1" -type f -not -name 'checkup-end-receipt.*' | wc -l | tr -d ' ')
43
+ check "dry run deleted nothing" "$AFTER" "$BEFORE"
44
+ check "dry run still has breaks.json" "$([ -f "$T/p1/analysis/breaks.json" ] && echo yes || echo no)" "yes"
45
+ R="$T/p1/checkup-end-receipt.json"
46
+ check "receipt json written" "$([ -f "$R" ] && echo yes || echo no)" "yes"
47
+ check "receipt md written" "$([ -f "$T/p1/checkup-end-receipt.md" ] && echo yes || echo no)" "yes"
48
+ check "receipt dryRun" "$(jq -r '.dryRun' "$R")" "true"
49
+ check "receipt business" "$(jq -r '.business' "$R")" "Oak Pottery"
50
+ check "receipt next = unpublish with the app id" "$(printf '%s' "$OUT" | jq -r '.next[0]')" "publish-app --app 28au74d9cj --delete"
51
+ check "receipt lists the apps to revoke" "$(jq -c '.todo[1].apps' "$R")" '["Square","Jotform","Jackrabbit"]'
52
+ check "receipt says names were not hashed" "$(jq -r '.namesHashed' "$R")" "false"
53
+ check "md mentions dry run" "$(grep -c 'dry run' "$T/p1/checkup-end-receipt.md")" "1"
54
+
55
+ # 2. Real run: known files gone, empty folders removed, leftovers kept and listed
56
+ build_checkup "$T/p2" hash
57
+ echo "owner note" > "$T/p2/analysis/notes.txt" # not written by a check-up skill → leftover
58
+ mkdir -p "$T/p2/ingest/raw" # a sub-folder → leftover
59
+ ln -s "$T/p2/intake.json" "$T/p2/checkup-data/link.json" # a symlink → never followed
60
+ OUT="$(bash "$EXEC" --dir "$T/p2" --business "Oak Pottery")"; RC=$?
61
+ check "end exits 0" "$RC" "0"
62
+ check "end success" "$(printf '%s' "$OUT" | jq -r '.success')" "true"
63
+ check "end not a dry run" "$(printf '%s' "$OUT" | jq -r '.dryRun')" "false"
64
+ check "breaks.json gone" "$([ -e "$T/p2/analysis/breaks.json" ] && echo still || echo gone)" "gone"
65
+ check "ingested rows gone" "$(ls "$T/p2/ingest" 2>/dev/null | grep -c '\.json$')" "0"
66
+ check "report-doc gone" "$([ -e "$T/p2/checkup-data/report-doc.json" ] && echo still || echo gone)" "gone"
67
+ check "daily gone" "$([ -e "$T/p2/checkup-data/daily-2026-10-27.json" ] && echo still || echo gone)" "gone"
68
+ check "timelog gone" "$([ -e "$T/p2/checkup-data/timelog.json" ] && echo still || echo gone)" "gone"
69
+ check "intake gone" "$([ -e "$T/p2/intake.json" ] && echo still || echo gone)" "gone"
70
+ check "app page copy gone" "$([ -e "$T/p2/checkup-app/index.html" ] && echo still || echo gone)" "gone"
71
+ check "empty checkup-app folder removed" "$([ -d "$T/p2/checkup-app" ] && echo still || echo gone)" "gone"
72
+ check "leftover note kept" "$([ -f "$T/p2/analysis/notes.txt" ] && echo yes || echo no)" "yes"
73
+ check "leftover folder kept" "$([ -d "$T/p2/ingest/raw" ] && echo yes || echo no)" "yes"
74
+ check "symlink kept (not followed)" "$([ -L "$T/p2/checkup-data/link.json" ] && echo yes || echo no)" "yes"
75
+ check "non-empty folders kept" "$([ -d "$T/p2/analysis" ] && [ -d "$T/p2/ingest" ] && echo yes || echo no)" "yes"
76
+ R="$T/p2/checkup-end-receipt.json"
77
+ check "receipt leftovers = 3" "$(jq -r '.leftovers | length' "$R")" "3"
78
+ check "receipt leftover note reason" "$(jq -r '.leftovers[] | select(.path=="analysis/notes.txt") | .note' "$R")" "not written by a check-up skill"
79
+ check "receipt symlink reason" "$(jq -r '.leftovers[] | select(.path=="checkup-data/link.json") | .note' "$R")" "symbolic link, not followed"
80
+ check "receipt names hashed" "$(jq -r '.namesHashed' "$R")" "true"
81
+ check "receipt people files counted" "$(printf '%s' "$OUT" | jq -r '.peopleFiles >= 8')" "true"
82
+ check "receipt bytes > 0" "$(jq -r '.bytes > 0' "$R")" "true"
83
+ check "receipt removed folders lists checkup-app" "$(jq -r '.removedFolders | index("checkup-app") != null' "$R")" "true"
84
+ check "receipt next without --app shows placeholder" "$(printf '%s' "$OUT" | jq -r '.next[0]')" "publish-app --app <appId> --delete"
85
+ check "md lists deleted kinds" "$(grep -c 'held people data' "$T/p2/checkup-end-receipt.md" | awk '{print ($1>=5)?"yes":"no"}')" "yes"
86
+ check "md hashed note" "$(grep -c 'were hashed' "$T/p2/checkup-end-receipt.md")" "1"
87
+
88
+ # 3. Raw exports folder: only *.csv in it, not recursive
89
+ build_checkup "$T/p3"
90
+ mkdir -p "$T/exports/sub"
91
+ cp "$FX"/*.csv "$T/exports/"
92
+ echo "x" > "$T/exports/readme.txt"
93
+ cp "$FX/square-transactions.csv" "$T/exports/sub/"
94
+ N_CSV=$(ls "$T/exports"/*.csv | wc -l | tr -d ' ')
95
+ OUT="$(bash "$EXEC" --dir "$T/p3" --exports "$T/exports")"; RC=$?
96
+ check "exports exits 0" "$RC" "0"
97
+ check "csv exports gone" "$(ls "$T/exports" | grep -c '\.csv$')" "0"
98
+ check "readme kept" "$([ -f "$T/exports/readme.txt" ] && echo yes || echo no)" "yes"
99
+ check "sub-folder csv kept" "$([ -f "$T/exports/sub/square-transactions.csv" ] && echo yes || echo no)" "yes"
100
+ check "receipt counts the csvs" "$(jq -r --argjson n "$N_CSV" '[.deleted[] | select(.kind | startswith("raw export"))] | length == $n' "$T/p3/checkup-end-receipt.json")" "true"
101
+
102
+ # 4. Refuses a folder that is not a check-up, deletes nothing
103
+ mkdir -p "$T/notme/analysis"; echo "keep" > "$T/notme/analysis/report.md"; echo "keep" > "$T/notme/other.txt"
104
+ OUT="$(bash "$EXEC" --dir "$T/notme")"; RC=$?
105
+ check "not a check-up exits 2" "$RC" "2"
106
+ check "not a check-up error" "$(printf '%s' "$OUT" | jq -r '.error' | grep -c 'does not look like a check-up')" "1"
107
+ check "not a check-up kept report.md" "$([ -f "$T/notme/analysis/report.md" ] && echo yes || echo no)" "yes"
108
+ check "not a check-up wrote no receipt" "$([ -e "$T/notme/checkup-end-receipt.json" ] && echo yes || echo no)" "no"
109
+
110
+ # 5. Missing folder, usage, JSON input, explicit slot flags and --receipt
111
+ OUT="$(bash "$EXEC" --dir "$T/nope")"; RC=$?
112
+ check "missing dir exits 2" "$RC" "2"
113
+ OUT="$(bash "$EXEC")"; RC=$?
114
+ check "no args exits 1" "$RC" "1"
115
+ OUT="$(bash "$EXEC" --dir "$T/p3" --bogus)"; RC=$?
116
+ check "unknown flag exits 1" "$RC" "1"
117
+ build_checkup "$T/p5"
118
+ mv "$T/p5/checkup-data" "$T/p5/elsewhere"
119
+ OUT="$(bash "$EXEC" "{\"dir\":\"$T/p5\",\"data\":\"$T/p5/elsewhere\",\"business\":\"Oak Pottery\",\"app\":\"abc123\",\"receipt\":\"$T/p5-receipt\",\"dryRun\":true}")"; RC=$?
120
+ check "json exits 0" "$RC" "0"
121
+ check "json dry run" "$(printf '%s' "$OUT" | jq -r '.dryRun')" "true"
122
+ check "json app in next" "$(printf '%s' "$OUT" | jq -r '.next[0]')" "publish-app --app abc123 --delete"
123
+ check "json --data slot found the moved folder" "$(jq -r '[.deleted[] | select(.path=="elsewhere/report-doc.json")] | length' "$T/p5-receipt/checkup-end-receipt.json")" "1"
124
+ check "json receipt in --receipt dir" "$([ -f "$T/p5-receipt/checkup-end-receipt.md" ] && echo yes || echo no)" "yes"
125
+ check "json receipt not in project" "$([ -e "$T/p5/checkup-end-receipt.json" ] && echo yes || echo no)" "no"
126
+
127
+ # 6. Ending twice: the second run finds nothing and says so
128
+ build_checkup "$T/p6"
129
+ bash "$EXEC" --dir "$T/p6" >/dev/null
130
+ OUT="$(bash "$EXEC" --dir "$T/p6")"; RC=$?
131
+ check "second end refuses (nothing left to mark a check-up)" "$RC" "2"
132
+
133
+ echo "checkup-end: $PASS passed, $FAIL failed"
134
+ [ "$FAIL" -eq 0 ]
@@ -106,6 +106,14 @@ and any step still open). A second `--start` on an open step restarts it;
106
106
  `--end` with no open entry fails (exit 2) so a forgotten start is noticed.
107
107
  `--at <iso>` back-fills a time you know. One `daily` entry per day is fine.
108
108
 
109
+ ## End — when the 7 days are over
110
+
111
+ Run `checkup-end --dir . --business "<Business>" --app <appId>`: it deletes
112
+ every file the check-up left on this machine (ingested rows, `breaks.json`,
113
+ the report and daily documents, the intake answers, the time log, the App
114
+ page copy) and writes a receipt. Then `publish-app --app <appId> --delete`
115
+ and send the owner the receipt. See that skill.
116
+
109
117
  ## Report back
110
118
 
111
119
  Tell the owner in plain words: the App is ready (the card has the link), the
@@ -88,8 +88,8 @@ Children's names, parents' emails and phone numbers are in most of these
88
88
  exports. Run with `--hash-names` unless the analysis truly needs the names:
89
89
  `name`, `email`, `phone` and every original column that looks personal become
90
90
  a short stable hash (`h:3f1a…`), so the same person still counts once across
91
- files but nobody's name is stored. Report counts, not names, and delete the
92
- `--out` folder when the check-up ends.
91
+ files but nobody's name is stored. Report counts, not names, and run
92
+ `checkup-end` when the check-up is over so the `--out` folder goes with a receipt.
93
93
 
94
94
  ## Report back
95
95
 
@@ -6,7 +6,7 @@
6
6
  {
7
7
  "name": "Checkup Lead",
8
8
  "role": "product-manager",
9
- "systemPrompt": "You run the small-business check-up end to end for one business owner. The four steps are intake, connect, analyse, report; log the start and end of each one with the 'checkup-report' skill (--log-step <step> --start / --end --out ./checkup-data) so the owner can see how long the check-up really took. Intake: run 'checkup-report --app-dir ./checkup-app', then 'publish-app' so the owner gets the App on their phone, and wait for the five answers (app-data --get intake answers). Connect and analyse: hand the Data Analyst the list of apps from the answers and the exports or connector access the owner provides; the analyst returns the path to breaks.json. Report: build the report document with 'checkup-report --breaks ... --business ... --intake ... --out ./checkup-data'. If a Reviewer is on the team, send them the report-doc.json path and wait for 'approved' or a list of fixes before publishing; if there is no Reviewer, check the document yourself and publish. Publish with 'app-data --set report current' and share the App link. Then post one daily summary for 7 days with 'checkup-report --daily'. Tell the owner the top 3 in plain words with count and estimate, say that the dollars are estimates from listed assumptions, and name the one thing you would start on. Never paste the whole list of people into chat or the App, and never send anything to a third party without the owner's say-so."
9
+ "systemPrompt": "You run the small-business check-up end to end for one business owner. The four steps are intake, connect, analyse, report; log the start and end of each one with the 'checkup-report' skill (--log-step <step> --start / --end --out ./checkup-data) so the owner can see how long the check-up really took. Intake: run 'checkup-report --app-dir ./checkup-app', then 'publish-app' so the owner gets the App on their phone, and wait for the five answers (app-data --get intake answers). Connect and analyse: hand the Data Analyst the list of apps from the answers and the exports or connector access the owner provides; the analyst returns the path to breaks.json. Report: build the report document with 'checkup-report --breaks ... --business ... --intake ... --out ./checkup-data'. If a Reviewer is on the team, send them the report-doc.json path and wait for 'approved' or a list of fixes before publishing; if there is no Reviewer, check the document yourself and publish. Publish with 'app-data --set report current' and share the App link. Then post one daily summary for 7 days with 'checkup-report --daily'. End: after the seventh daily line, or when the owner says the check-up is over, run 'checkup-end --dir . --business <name> --app <appId>' (--dry-run first if unsure; add --exports <dir> when the owner handed over CSV files) so every ingested row, the analysis and the report leave this machine, then 'publish-app --app <appId> --delete', and send the owner the receipt it wrote in plain words. Tell the owner the top 3 in plain words with count and estimate, say that the dollars are estimates from listed assumptions, and name the one thing you would start on. Never paste the whole list of people into chat or the App, and never send anything to a third party without the owner's say-so."
10
10
  },
11
11
  {
12
12
  "name": "Data Analyst",