@cspeach/cli 1.1.30 → 1.1.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -1
- package/dist/agent/loop.js +132 -9
- package/dist/cli.js +49 -18
- package/dist/commands/config-set.js +1 -1
- package/dist/commands/help.js +16 -1
- package/dist/commands/plan-chain.js +3 -0
- package/dist/commands/plan-continue.js +1 -1
- package/dist/commands/plan-gate.js +17 -1
- package/dist/commands/plan-resume.js +28 -18
- package/dist/commands/plan-startup.js +49 -6
- package/dist/commands/team-discover.js +195 -0
- package/dist/commands/team-plan.js +585 -0
- package/dist/commands/team-rules.js +187 -0
- package/dist/commands/team.js +971 -95
- package/dist/journey/chain.js +978 -0
- package/dist/journey/clean-core.js +235 -0
- package/dist/journey/flow.js +216 -0
- package/dist/journey/rebuild.js +464 -0
- package/dist/journey/run.js +628 -0
- package/dist/journey/session-deps.js +81 -0
- package/dist/journey/trigger.js +153 -0
- package/dist/lane/app-chain.js +21 -9
- package/dist/lane/chain-token.js +9 -5
- package/dist/lane/roots.js +97 -41
- package/dist/one-shot.js +28 -2
- package/dist/projects/clean-core-confirm.js +192 -3
- package/dist/projects/detail-path.js +9 -4
- package/dist/projects/extract-clean-core.js +3 -0
- package/dist/projects/migration.js +4 -105
- package/dist/projects/plan-lane.js +67 -17
- package/dist/projects/plan-sources.js +13 -7
- package/dist/projects/project-folder.js +131 -0
- package/dist/projects/promote-command.js +39 -0
- package/dist/projects/save-command.js +100 -12
- package/dist/projects/saved-lines.js +17 -3
- package/dist/projects/sort-command.js +499 -0
- package/dist/projects/sort-core.js +248 -0
- package/dist/projects/workspace-retire.js +48 -0
- package/dist/projects/workspace.js +182 -121
- package/dist/register/baseline.js +39 -2
- package/dist/register/decision-rules.js +35 -0
- package/dist/register/discover.js +931 -0
- package/dist/register/export-files.js +58 -0
- package/dist/register/findings.js +5 -2
- package/dist/register/measure.js +191 -9
- package/dist/register/offer.js +100 -8
- package/dist/register/picture.js +6 -2
- package/dist/register/plan-import.js +328 -0
- package/dist/register/records.js +40 -3
- package/dist/register/rule-proposal.js +4 -0
- package/dist/register/rules-edit.js +257 -0
- package/dist/register/status-file/estate-parts.js +168 -0
- package/dist/register/status-file/plan-parts.js +68 -0
- package/dist/register/status-file/progress-chart.js +110 -0
- package/dist/register/status-file/register-map.js +11 -3
- package/dist/register/status-file/register-model.js +40 -6
- package/dist/register/status-file/register-parts.js +57 -11
- package/dist/register/status-file/register-status-file.js +106 -90
- package/dist/register/status-file/report-model.js +569 -0
- package/dist/register/status-file/team-css.js +1 -1
- package/dist/register/status-file-sync.js +4 -0
- package/dist/register/status-html.js +27 -6
- package/dist/register/store.js +44 -5
- package/dist/register/variant.js +65 -0
- package/dist/register/workbooks.js +28 -42
- package/dist/register/write-whole.js +30 -2
- package/dist/register/xlsx.js +13 -3
- package/dist/register/xref.js +117 -0
- package/dist/renderer/startup-lines.js +11 -0
- package/dist/renderer/tool-labels.js +3 -1
- package/dist/renderer/tool-widget.js +15 -1
- package/dist/repl/at-completer.js +6 -2
- package/dist/repl/at-picker.js +1 -1
- package/dist/repl/builtin-commands.js +4 -0
- package/dist/repl/journey-entry.js +27 -0
- package/dist/repl/plan-hop.js +2 -9
- package/dist/repl/rules-menu.js +177 -0
- package/dist/repl/slash-completer.js +1 -0
- package/dist/repl/slash-picker.js +5 -4
- package/dist/repl.js +236 -59
- package/dist/router/classifier.js +8 -4
- package/dist/router/router-core.js +43 -7
- package/dist/router/routing-decision.js +1 -1
- package/dist/runs/bundled-runs.js +100 -0
- package/dist/runs/front-door.js +19 -0
- package/dist/runs/manifest-runs.js +50 -0
- package/dist/runs/run-command.js +24 -0
- package/dist/runs/runs.js +27 -0
- package/dist/runs/shadow.js +100 -0
- package/dist/runs/types.js +1 -0
- package/dist/session/backfill.js +342 -0
- package/dist/session/project-match.js +63 -0
- package/dist/session/queued-step.js +45 -0
- package/dist/session/resume.js +72 -24
- package/dist/session/schema.js +3 -1
- package/dist/session/store.js +96 -51
- package/dist/skill-catalog.js +2 -2
- package/dist/skills/bundled-skills.js +53 -53
- package/dist/tools/_filesystem-shared.js +174 -12
- package/dist/tools/_team-work-path.js +133 -0
- package/dist/tools/approval.js +7 -1
- package/dist/tools/dispatch-skill.js +8 -4
- package/dist/tools/filesystem/extract-xlsx.js +27 -3
- package/dist/tools/filesystem/file-edit.js +12 -4
- package/dist/tools/filesystem/file-read.js +8 -4
- package/dist/tools/filesystem/file-write.js +12 -4
- package/dist/tools/filesystem/glob.js +33 -8
- package/dist/tools/filesystem/grep.js +9 -4
- package/dist/tools/filesystem/read-document.js +6 -3
- package/dist/tools/lane/app-lane-resolve.js +1 -1
- package/dist/tools/project/cca_merge.js +8 -2
- package/dist/tools/project/rules_propose.js +59 -0
- package/dist/tools/project/team_estate.js +91 -0
- package/dist/tools/project/team_findings.js +89 -5
- package/dist/tools/project/upgrade_merge_progress.js +8 -2
- package/dist/tools/sap-read.js +21 -4
- package/dist/tools/sap-write.js +200 -10
- package/dist/tools/subagent/read_agent.js +3 -0
- package/dist/tools/tool-loading.js +1 -1
- package/dist/tools/transport-fit-check.js +117 -0
- package/dist/tools/transport-fit.js +15 -7
- package/dist/tools/transport.js +57 -2
- package/dist/ui/app.js +2 -2
- package/dist/ui/approval-modal.js +74 -6
- package/dist/ui/widgets/confirm-card.js +68 -5
- package/dist/upgrade-check.js +49 -4
- package/dist/util/open-in-browser.js +143 -0
- package/package.json +3 -3
|
@@ -1,15 +1,45 @@
|
|
|
1
|
+
// cspeach-cli/src/commands/plan-startup.ts
|
|
2
|
+
//
|
|
3
|
+
// 2026-10-04 (continuous run, spec §3.5) — typing `cspeach` in the project
|
|
4
|
+
// folder offers to continue an unfinished plan:
|
|
5
|
+
// one plan → "Continue 'FI Document Aging' (3 of 6 done, next: Service)?"
|
|
6
|
+
// several → "Continue an unfinished plan?", newest first (max 5), + "Not now"
|
|
7
|
+
// Approvals do not survive a restart (the session is new), so Continue shows
|
|
8
|
+
// the plan's object list again and installs a fresh object-list grant
|
|
9
|
+
// (installPlanGrant: same policy as "Build this now?"), then submits the plain
|
|
10
|
+
// resume command (no mode flag: the user's plan_mode decides).
|
|
11
|
+
//
|
|
12
|
+
// F13 — a resume the user typed in guarded mode, with no grant for that plan,
|
|
13
|
+
// shows the same object list once before the step (offerObjectListOnResume).
|
|
14
|
+
// Yes installs the grant; No runs the step with a card per write. Never on a
|
|
15
|
+
// chain-queued hop, never twice for one Continue.
|
|
16
|
+
import { basename, dirname } from 'node:path';
|
|
1
17
|
import { readProjectFile } from '../projects/status.js';
|
|
2
18
|
import { parsePlanContent } from '../projects/plan-schema.js';
|
|
3
19
|
import { computeNextPhaseForRun, isPhaseSatisfiedForRun, phaseDisplayTitle, statusesFromItems, } from '../projects/plan-run.js';
|
|
4
20
|
import { collectPlannedObjects, plannedObjectFacts, planGrantFactLines } from '../projects/plan-objects.js';
|
|
5
21
|
import { adoptPlanTransportOnResume, buildResumeCommand, planBuildTransport } from './plan-resume.js';
|
|
6
22
|
import { newestPlanFamilyFiles } from '../session/recap.js';
|
|
23
|
+
import { legacyDirsFor } from '../projects/project-folder.js';
|
|
7
24
|
import { getPlanGrant, installPlanGrant, planGrantAllowed } from '../approvals/plan-grant.js';
|
|
8
25
|
import { DEFAULT_PLAN_MODE } from './plan-gate.js';
|
|
9
|
-
/**
|
|
10
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Plans in the folder with a step left to build, newest first. With `cwd`,
|
|
28
|
+
* the old save locations for that folder are read too (Task 11, one
|
|
29
|
+
* release), after the new folder; a same-name file in the new folder wins.
|
|
30
|
+
*/
|
|
31
|
+
export function listUnfinishedPlans(workspaceDir, max = 5, cwd) {
|
|
11
32
|
const out = [];
|
|
12
|
-
|
|
33
|
+
const dirs = [workspaceDir, ...(cwd ? safeLegacyDirs(cwd) : [])];
|
|
34
|
+
const seen = new Set();
|
|
35
|
+
const files = dirs.flatMap((d, i) => newestPlanFamilyFiles(d).map((f) => ({ ...f, earlier: i > 0 }))).filter((f) => {
|
|
36
|
+
const name = basename(f.path).toLowerCase();
|
|
37
|
+
if (seen.has(name))
|
|
38
|
+
return false;
|
|
39
|
+
seen.add(name);
|
|
40
|
+
return true;
|
|
41
|
+
});
|
|
42
|
+
for (const f of files) {
|
|
13
43
|
if (out.length >= max)
|
|
14
44
|
break;
|
|
15
45
|
let env;
|
|
@@ -42,13 +72,23 @@ export function listUnfinishedPlans(workspaceDir, max = 5) {
|
|
|
42
72
|
nextTitle: phaseDisplayTitle(next, phases),
|
|
43
73
|
objects: collectPlannedObjects(pc.content),
|
|
44
74
|
...(transport ? { transport } : {}),
|
|
75
|
+
...(f.earlier ? { earlier: true } : {}),
|
|
45
76
|
});
|
|
46
77
|
}
|
|
47
78
|
return out;
|
|
48
79
|
}
|
|
80
|
+
function safeLegacyDirs(cwd) {
|
|
81
|
+
try {
|
|
82
|
+
return legacyDirsFor(cwd);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
49
88
|
function progress(p) {
|
|
50
89
|
return `${p.done} of ${p.total} done, next: ${p.nextTitle ?? 'the independent check'}`;
|
|
51
90
|
}
|
|
91
|
+
const EARLIER = '(earlier location)';
|
|
52
92
|
export function continueQuestion(p) {
|
|
53
93
|
return `Continue '${p.title}' (${progress(p)})?`;
|
|
54
94
|
}
|
|
@@ -97,7 +137,7 @@ function grantInstalledFor(planId, sessionId) {
|
|
|
97
137
|
&& typeof g.expiresAt === 'number' && Date.now() < g.expiresAt;
|
|
98
138
|
}
|
|
99
139
|
export async function offerContinueAtStartup(a) {
|
|
100
|
-
const plans = listUnfinishedPlans(a.workspaceDir);
|
|
140
|
+
const plans = listUnfinishedPlans(a.workspaceDir, 5, a.cwd);
|
|
101
141
|
if (plans.length === 0)
|
|
102
142
|
return 'none';
|
|
103
143
|
const allowed = a.grantAllowed ?? planGrantAllowed;
|
|
@@ -109,7 +149,10 @@ export async function offerContinueAtStartup(a) {
|
|
|
109
149
|
grantable = p.objects.length > 0 && await allowed(a.sapAlias).catch(() => false);
|
|
110
150
|
const v = await a.choose({
|
|
111
151
|
title: continueQuestion(p),
|
|
112
|
-
facts:
|
|
152
|
+
facts: [
|
|
153
|
+
...(p.earlier ? [`This plan is in an earlier location: ${dirname(p.path)}`] : []),
|
|
154
|
+
...objectListFacts(p.objects, p.transport, grantable),
|
|
155
|
+
],
|
|
113
156
|
choices: [{ value: 'continue', label: 'Continue' }, { value: 'later', label: 'Not now' }],
|
|
114
157
|
});
|
|
115
158
|
if (v === 'continue') {
|
|
@@ -122,7 +165,7 @@ export async function offerContinueAtStartup(a) {
|
|
|
122
165
|
title: 'Continue an unfinished plan?',
|
|
123
166
|
facts: ['Newest first.'],
|
|
124
167
|
choices: [
|
|
125
|
-
...plans.map((p, i) => ({ value: String(i), label: `${p.title} (${progress(p)})` })),
|
|
168
|
+
...plans.map((p, i) => ({ value: String(i), label: `${p.title} (${progress(p)})${p.earlier ? ` ${EARLIER}` : ''}` })),
|
|
126
169
|
{ value: 'later', label: 'Not now' },
|
|
127
170
|
],
|
|
128
171
|
});
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cspeach team discover` — the estate inventory before anyone chose a scope
|
|
3
|
+
* (spec docs/superpowers/specs/2026-10-06-cca-discovery-team-plan-design.md §5, §7).
|
|
4
|
+
*
|
|
5
|
+
* Read only: every statement is a fixed one in register/discover.ts and goes
|
|
6
|
+
* through sap-client's read-only gate. Files go into `<folder>/discovery/`,
|
|
7
|
+
* each written whole then renamed; older discoveries are never deleted (the
|
|
8
|
+
* newest stamp wins on read). On a team project folder only its lead may run
|
|
9
|
+
* it, and that is checked before SAP is even reached.
|
|
10
|
+
*/
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { createHash } from 'node:crypto';
|
|
14
|
+
import { NO_USAGE_LINE, estateSheets, estateSummaryOf, objectKey, usageLineWithWindow } from '@cspeach/register-core';
|
|
15
|
+
import { discover } from '../register/discover.js';
|
|
16
|
+
import { DISCOVERY_DIR, RegisterError, TEAM_FILE, loadState, readBaseline, readProjectFile } from '../register/store.js';
|
|
17
|
+
import { fileStamp, writeRecord } from '../register/records.js';
|
|
18
|
+
import { writeWhole } from '../register/write-whole.js';
|
|
19
|
+
import { writeXlsxFile } from '../register/xlsx.js';
|
|
20
|
+
import { HELD } from '../register/workbooks.js';
|
|
21
|
+
import { flags, list, onInterrupt, requireLead, whoAmI } from './team.js';
|
|
22
|
+
export const DISCOVER_USAGE = 'Usage: cspeach team discover [--out <folder>] [--prefixes Z,Y] [--namespaces auto|/ACME/,…] [--sap-alias S4H] '
|
|
23
|
+
+ '[--no-xref] [--no-jobs] [--max-minutes 30]';
|
|
24
|
+
const DISCOVER_OPTIONS = ['out', 'prefixes', 'namespaces', 'sap-alias', 'no-xref', 'no-jobs', 'max-minutes'];
|
|
25
|
+
const DISCOVER_FLAGS = ['no-xref', 'no-jobs'];
|
|
26
|
+
/** 2,931 — every count on the estate line reads the way a person writes it. */
|
|
27
|
+
const n = (v) => v.toLocaleString('en-US');
|
|
28
|
+
/** The usage part of the estate line, from the summary's one sentence. */
|
|
29
|
+
function usageWords(s) {
|
|
30
|
+
if (s.usageLine === NO_USAGE_LINE)
|
|
31
|
+
return 'none recorded on this system';
|
|
32
|
+
return usageLineWithWindow(s).replace(/^Usage /, '').replace(/\.$/, '');
|
|
33
|
+
}
|
|
34
|
+
export function estateLine(s) {
|
|
35
|
+
const t = s.totals;
|
|
36
|
+
return `Estate: ${n(t.packages)} packages · ${n(t.codeObjects)} code objects · ${n(t.nonCodeObjects)} non-code · `
|
|
37
|
+
+ `${n(t.local)} outside packages · usage: ${usageWords(s)} · modifications of SAP objects: ${n(t.modifications)} · unknowns: ${n(t.unknowns)}`;
|
|
38
|
+
}
|
|
39
|
+
/** The workbook sheets of an estate summary, as the xlsx writer takes them. */
|
|
40
|
+
export function estateWorkbook(s) {
|
|
41
|
+
return estateSheets(s).map((sh) => ({ name: sh.name, header: sh.table.header, rows: sh.table.rows }));
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The evidence rows of an objects file, for the objects a baseline holds.
|
|
45
|
+
* Rows for anything else are not written; they are counted.
|
|
46
|
+
*/
|
|
47
|
+
export function evidenceRows(objects, baselineKeys) {
|
|
48
|
+
const list = objects?.objects;
|
|
49
|
+
if (objects?.kind !== 'estate-objects' || !Array.isArray(list))
|
|
50
|
+
return null;
|
|
51
|
+
const rows = [];
|
|
52
|
+
const seen = new Set();
|
|
53
|
+
let outside = 0;
|
|
54
|
+
for (const o of list) {
|
|
55
|
+
if (!o || typeof o.type !== 'string' || typeof o.name !== 'string')
|
|
56
|
+
continue;
|
|
57
|
+
const key = objectKey(o.type, o.name);
|
|
58
|
+
if (!baselineKeys.has(key)) {
|
|
59
|
+
outside += 1;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (seen.has(key))
|
|
63
|
+
continue;
|
|
64
|
+
seen.add(key);
|
|
65
|
+
rows.push({ key, callers: o.callers, batch: o.batch, usage: o.usage, changed: o.changed });
|
|
66
|
+
}
|
|
67
|
+
return { rows, outside };
|
|
68
|
+
}
|
|
69
|
+
export const EVIDENCE_LINE = (count) => `Evidence for ${count} objects written (callers, batch runs, usage, last change).`;
|
|
70
|
+
export const OUTSIDE_LINE = (count) => `${count === 1 ? '1 object' : `${count} objects`} in the inventory ${count === 1 ? 'is' : 'are'} outside this team project.`;
|
|
71
|
+
/** One sentence for a file that could not be written: its name and the code, never the path (it carries the user name). */
|
|
72
|
+
const cannotWrite = (name, code) => `Could not write ${name} in the discovery folder${code ? ` (${code})` : ''}.`;
|
|
73
|
+
function writeIn(name, write) {
|
|
74
|
+
try {
|
|
75
|
+
write();
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
throw new RegisterError(cannotWrite(name, e?.code ?? ''));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The stamp, or `<stamp>-2`, `-3`, … when a discovery of the same second is
|
|
83
|
+
* already there: an older discovery is history and is never overwritten.
|
|
84
|
+
*/
|
|
85
|
+
function freeStamp(folder, stamp) {
|
|
86
|
+
const taken = (s) => fs.existsSync(path.join(folder, `estate-${s}.json`)) || fs.existsSync(path.join(folder, `objects-${s}.json`));
|
|
87
|
+
if (!taken(stamp))
|
|
88
|
+
return stamp;
|
|
89
|
+
for (let i = 2;; i += 1)
|
|
90
|
+
if (!taken(`${stamp}-${i}`))
|
|
91
|
+
return `${stamp}-${i}`;
|
|
92
|
+
}
|
|
93
|
+
/** The same folder, said two ways: on Windows case does not matter, and a trailing separator never does. */
|
|
94
|
+
export function samePath(a, b) {
|
|
95
|
+
const norm = (p) => {
|
|
96
|
+
const r = path.resolve(p).replace(/[\\/]+$/, '');
|
|
97
|
+
return process.platform === 'win32' ? r.toLowerCase() : r;
|
|
98
|
+
};
|
|
99
|
+
return norm(a) === norm(b);
|
|
100
|
+
}
|
|
101
|
+
export async function teamDiscover(args, deps) {
|
|
102
|
+
const { positional, opts } = flags(args, DISCOVER_FLAGS);
|
|
103
|
+
for (const name of Object.keys(opts)) {
|
|
104
|
+
if (!DISCOVER_OPTIONS.includes(name))
|
|
105
|
+
throw new RegisterError(`Unknown option --${name}.\n${DISCOVER_USAGE}`);
|
|
106
|
+
}
|
|
107
|
+
// 1.1.31: no --out → the folder you are in (owner: "why doesn't CSPeach remember all that?").
|
|
108
|
+
if (positional.length || opts.out === 'true')
|
|
109
|
+
throw new RegisterError(DISCOVER_USAGE);
|
|
110
|
+
const maxMinutes = opts['max-minutes'] === undefined ? 30 : Number(opts['max-minutes']);
|
|
111
|
+
if (!Number.isFinite(maxMinutes) || maxMinutes <= 0)
|
|
112
|
+
throw new RegisterError(`--max-minutes needs a number of minutes, for example: --max-minutes 30\n${DISCOVER_USAGE}`);
|
|
113
|
+
const namespaces = !opts.namespaces || opts.namespaces === 'true' || opts.namespaces.trim().toLowerCase() === 'auto'
|
|
114
|
+
? 'auto' : list(opts.namespaces).map((s) => s.toUpperCase());
|
|
115
|
+
const prefixes = opts.prefixes && opts.prefixes !== 'true' ? list(opts.prefixes).map((s) => s.toUpperCase()) : ['Z', 'Y'];
|
|
116
|
+
const dir = path.resolve(opts.out ?? process.cwd());
|
|
117
|
+
// A team project folder: its lead only, and that is settled before SAP is reached.
|
|
118
|
+
const isTeam = fs.existsSync(path.join(dir, TEAM_FILE));
|
|
119
|
+
const project = isTeam ? readProjectFile(dir) : null;
|
|
120
|
+
if (project)
|
|
121
|
+
requireLead(project, deps);
|
|
122
|
+
if (deps.noSap || !deps.connect)
|
|
123
|
+
throw new RegisterError('discover needs a SAP connection to read the estate.');
|
|
124
|
+
const { sql, system } = await deps.connect(opts['sap-alias']);
|
|
125
|
+
const stop = new AbortController();
|
|
126
|
+
const off = onInterrupt(deps, () => {
|
|
127
|
+
if (stop.signal.aborted)
|
|
128
|
+
return;
|
|
129
|
+
deps.print(' Stopping. What has been read so far will still be written.');
|
|
130
|
+
stop.abort();
|
|
131
|
+
});
|
|
132
|
+
let result;
|
|
133
|
+
try {
|
|
134
|
+
deps.print('Reading the estate from SAP (read only). Ctrl-C keeps what has been read.');
|
|
135
|
+
result = await discover(sql, {
|
|
136
|
+
prefixes, namespaces, xref: !opts['no-xref'], jobs: !opts['no-jobs'], maxMinutes,
|
|
137
|
+
signal: stop.signal, by: whoAmI(deps), ...(system ? { system } : {}),
|
|
138
|
+
...(deps.now ? { now: deps.now } : {}),
|
|
139
|
+
onStep: (step, label) => deps.print(` Step ${step} of 12: ${label}`),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
off();
|
|
144
|
+
}
|
|
145
|
+
const folder = path.join(dir, DISCOVERY_DIR);
|
|
146
|
+
const stamp = freeStamp(folder, fileStamp(new Date(result.estate.takenAt)));
|
|
147
|
+
const objectsName = `objects-${stamp}.json`;
|
|
148
|
+
const estateName = `estate-${stamp}.json`;
|
|
149
|
+
const objectsText = `${JSON.stringify(result.objects, null, 1)}\n`;
|
|
150
|
+
// The object list first: an estate file on disk always has its object list beside it.
|
|
151
|
+
writeIn(objectsName, () => writeWhole(path.join(folder, objectsName), objectsText));
|
|
152
|
+
writeIn(estateName, () => writeWhole(path.join(folder, estateName), `${JSON.stringify(result.estate, null, 1)}\n`));
|
|
153
|
+
const summary = estateSummaryOf(result.estate, project ? readBaseline(dir) : undefined);
|
|
154
|
+
// Said as written only when it was: no summary, no workbook.
|
|
155
|
+
let workbook = !!summary;
|
|
156
|
+
let held = false;
|
|
157
|
+
try {
|
|
158
|
+
if (summary)
|
|
159
|
+
writeXlsxFile(path.join(folder, 'estate.xlsx'), estateWorkbook(summary));
|
|
160
|
+
}
|
|
161
|
+
catch (e) {
|
|
162
|
+
const code = e?.code ?? '';
|
|
163
|
+
if (!HELD.has(code))
|
|
164
|
+
throw new RegisterError(cannotWrite('estate.xlsx', code));
|
|
165
|
+
workbook = false;
|
|
166
|
+
held = true;
|
|
167
|
+
}
|
|
168
|
+
if (summary)
|
|
169
|
+
deps.print(estateLine(summary));
|
|
170
|
+
deps.print(` Written in the discovery folder: ${estateName}, ${objectsName}${workbook ? ', estate.xlsx' : ''}`);
|
|
171
|
+
if (held)
|
|
172
|
+
deps.print(' ! "estate.xlsx" is open in another program, so it was not updated. Close it and run discover again.');
|
|
173
|
+
if (!project) {
|
|
174
|
+
deps.print('Next: open the folder on the team page, or start a team project here: cspeach team start <packages>');
|
|
175
|
+
return 0;
|
|
176
|
+
}
|
|
177
|
+
// The lead's own team project folder: the inventory's evidence for its objects, as one record.
|
|
178
|
+
const joined = Object.entries(loadState().projects).find(([id, p]) => id === project.projectId && samePath(p.projectDir, dir));
|
|
179
|
+
if (!joined) {
|
|
180
|
+
deps.print(' No evidence recorded: this laptop has not joined this team project. Run: cspeach team join <folder>');
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
const [projectId, p] = joined;
|
|
184
|
+
const keys = new Set(readBaseline(dir).map((o) => objectKey(o.type, o.name)));
|
|
185
|
+
const ev = evidenceRows(result.objects, keys);
|
|
186
|
+
const res = writeRecord({ ...p, projectId }, 'evidence', ev.rows, { name: whoAmI(deps) }, {
|
|
187
|
+
dir: path.join(dir, 'writers', p.writerId, 'records'),
|
|
188
|
+
source: { file: objectsName, sha256: createHash('sha256').update(objectsText).digest('hex') },
|
|
189
|
+
...(deps.now ? { now: deps.now() } : {}), ...(deps.rand ? { rand: deps.rand } : {}),
|
|
190
|
+
});
|
|
191
|
+
deps.print(` ${EVIDENCE_LINE(res.written)}`);
|
|
192
|
+
if (ev.outside)
|
|
193
|
+
deps.print(` ${OUTSIDE_LINE(ev.outside)}`);
|
|
194
|
+
return 0;
|
|
195
|
+
}
|