@cspeach/cli 1.1.21 → 1.1.22
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/dist/sap/standalone-profile.js +1 -1
- package/dist/sap/system-info.js +50 -27
- package/dist/skills/bundled-skills.js +118 -118
- package/dist/upgrade.js +4 -2
- package/package.json +121 -121
- package/dist/commands/project.js +0 -301
- package/dist/tools/local-files.js +0 -31
- package/dist/tools/subagent/schedule_create.js +0 -131
- package/dist/ui/error-emitter.js +0 -21
- package/dist/ui/header.js +0 -41
- package/dist/ui/ink-classifier-route.js +0 -19
- package/dist/ui/rich-error-box.js +0 -9
- package/dist/ui/sidebar.js +0 -12
- package/dist/ui/status-emitter.js +0 -19
- package/dist/ui/status-line.js +0 -62
- package/dist/ui/status-row.js +0 -38
- package/dist/ui/todo-panel.js +0 -85
- package/dist/ui/widgets/ask-form.js +0 -250
- package/dist/ui/widgets/ask-question-modal.js +0 -178
- package/fiori/zlas-inspection-lr/package.json +0 -26
- package/fiori/zlas-inspection-lr/ui5-local.yaml +0 -45
- package/fiori/zlas-inspection-lr/ui5.yaml +0 -41
- package/fiori/zlas-inspection-lr/webapp/Component.js +0 -12
- package/fiori/zlas-inspection-lr/webapp/i18n/i18n.properties +0 -9
- package/fiori/zlas-inspection-lr/webapp/index.html +0 -35
- package/fiori/zlas-inspection-lr/webapp/manifest.json +0 -145
package/dist/upgrade.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { confirm } from '@inquirer/prompts';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
|
+
/** Our published package. Never the unscoped `cspeach` (not ours on npm). */
|
|
5
|
+
export const UPGRADE_PACKAGE = '@cspeach/cli@latest';
|
|
4
6
|
export async function runUpgrade() {
|
|
5
7
|
console.log(chalk.cyan('Upgrading cspeach to latest via npm…'));
|
|
6
|
-
const yes = await confirm({ message:
|
|
8
|
+
const yes = await confirm({ message: `Run \`npm i -g ${UPGRADE_PACKAGE}\`?`, default: true });
|
|
7
9
|
if (!yes)
|
|
8
10
|
return 0;
|
|
9
11
|
return new Promise((resolve) => {
|
|
10
|
-
const proc = spawn('npm', ['i', '-g',
|
|
12
|
+
const proc = spawn('npm', ['i', '-g', UPGRADE_PACKAGE], { stdio: 'inherit', shell: true });
|
|
11
13
|
proc.on('exit', (code) => resolve(code ?? 1));
|
|
12
14
|
});
|
|
13
15
|
}
|
package/package.json
CHANGED
|
@@ -1,121 +1,121 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@cspeach/cli",
|
|
3
|
-
"version": "1.1.
|
|
4
|
-
"description": "CSPeach — AI-assisted ABAP development CLI. Direct SAP access, 34 skills, safety gates.",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"abap",
|
|
7
|
-
"sap",
|
|
8
|
-
"ai",
|
|
9
|
-
"cli",
|
|
10
|
-
"ai-assisted-development",
|
|
11
|
-
"s4hana",
|
|
12
|
-
"abap-development",
|
|
13
|
-
"claude",
|
|
14
|
-
"anthropic"
|
|
15
|
-
],
|
|
16
|
-
"homepage": "https://cspeach.dev",
|
|
17
|
-
"bugs": {
|
|
18
|
-
"email": "laeeq.siddique@cremencing.com"
|
|
19
|
-
},
|
|
20
|
-
"author": "Laeeq Siddique (Cremencing Solutions)",
|
|
21
|
-
"license": "SEE LICENSE IN LICENSE",
|
|
22
|
-
"private": false,
|
|
23
|
-
"publishConfig": {
|
|
24
|
-
"access": "public"
|
|
25
|
-
},
|
|
26
|
-
"type": "module",
|
|
27
|
-
"bin": {
|
|
28
|
-
"cspeach": "dist/index.js"
|
|
29
|
-
},
|
|
30
|
-
"scripts": {
|
|
31
|
-
"dev": "tsx src/index.ts",
|
|
32
|
-
"prebuild": "tsx scripts/prebuild.ts",
|
|
33
|
-
"build": "tsc && node scripts/make-bin-executable.js",
|
|
34
|
-
"install:global": "tsx scripts/install-global.ts",
|
|
35
|
-
"pretest": "tsx scripts/sync-signing-key.ts || node -e \"console.warn('WARN: sync-signing-key skipped — using stub')\"",
|
|
36
|
-
"start": "node dist/index.js",
|
|
37
|
-
"typecheck": "tsc --noEmit",
|
|
38
|
-
"test": "vitest run",
|
|
39
|
-
"test:watch": "vitest",
|
|
40
|
-
"test:smoke-e2e": "vitest run --config vitest.smoke-e2e.config.ts",
|
|
41
|
-
"adt:smoke": "tsx scripts/adt-smoke.ts",
|
|
42
|
-
"smoke:all": "pnpm zcspeach:smoke && pnpm adt:smoke",
|
|
43
|
-
"zcspeach:smoke": "tsx scripts/zcspeach-smoke.ts",
|
|
44
|
-
"bench:models": "tsx scripts/bench-models.ts",
|
|
45
|
-
"bench:audit": "tsx scripts/bench-audit-task.ts",
|
|
46
|
-
"bench:replay": "tsx bench/replay/run.ts",
|
|
47
|
-
"fiori": "tsx src/tools/fiori/bin.ts",
|
|
48
|
-
"prepare": "husky",
|
|
49
|
-
"prepublishOnly": "node -e \"const deps=require('./package.json').dependencies||{}; for (const n of ['@cspeach/sap-client','@cspeach/register-core']) { const d=deps[n]||''; if(d.startsWith('file:')||d.startsWith('link:')){console.error('publish blocked: '+n+' is a local link ('+d+') — point it at the published version before npm publish'); process.exit(1)} }\""
|
|
50
|
-
},
|
|
51
|
-
"engines": {
|
|
52
|
-
"node": ">=20.0.0"
|
|
53
|
-
},
|
|
54
|
-
"dependencies": {
|
|
55
|
-
"@anthropic-ai/sdk": "^0.90.0",
|
|
56
|
-
"@cspeach/register-core": "0.1.0",
|
|
57
|
-
"@cspeach/sap-client": "0.1.15",
|
|
58
|
-
"@iarna/toml": "^2.2.5",
|
|
59
|
-
"@inkjs/ui": "^2.0.0",
|
|
60
|
-
"@inquirer/prompts": "^7.0.0",
|
|
61
|
-
"@napi-rs/keyring": "2.1.0",
|
|
62
|
-
"@noble/ed25519": "2.1.0",
|
|
63
|
-
"@noble/hashes": "1.4.0",
|
|
64
|
-
"@sap-ux/annotation-converter": "0.10.21",
|
|
65
|
-
"@sap-ux/edmx-parser": "0.10.0",
|
|
66
|
-
"@sap-ux/fe-fpm-writer": "0.43.7",
|
|
67
|
-
"@sap-ux/fiori-elements-writer": "^3.0.39",
|
|
68
|
-
"@sap-ux/fiori-freestyle-writer": "3.0.12",
|
|
69
|
-
"@sap-ux/vocabularies-types": "0.15.0",
|
|
70
|
-
"adm-zip": "0.5.17",
|
|
71
|
-
"chalk": "^5.3.0",
|
|
72
|
-
"cli-highlight": "^2.1.11",
|
|
73
|
-
"cli-table3": "^0.6.5",
|
|
74
|
-
"clipboardy": "^5.3.1",
|
|
75
|
-
"cross-spawn": "^7.0.6",
|
|
76
|
-
"diff": "^9.0.0",
|
|
77
|
-
"ejs": "3.1.10",
|
|
78
|
-
"fuse.js": "^7.0.0",
|
|
79
|
-
"ink": "^5.0.0",
|
|
80
|
-
"ink-text-input": "^6.0.0",
|
|
81
|
-
"jose": "^5.9.0",
|
|
82
|
-
"mammoth": "^1.8.0",
|
|
83
|
-
"marked": "^11.2.0",
|
|
84
|
-
"marked-terminal": "^6.2.0",
|
|
85
|
-
"mem-fs": "2.1.0",
|
|
86
|
-
"mem-fs-editor": "9.4.0",
|
|
87
|
-
"node-machine-id": "^1.1.12",
|
|
88
|
-
"openai": "^6.49.0",
|
|
89
|
-
"puppeteer-core": "24.10.2",
|
|
90
|
-
"react": "^18.3.0",
|
|
91
|
-
"string-width": "^7.2.0",
|
|
92
|
-
"turndown": "^7.2.0",
|
|
93
|
-
"unpdf": "1.7.0",
|
|
94
|
-
"zod": "^3.23.0"
|
|
95
|
-
},
|
|
96
|
-
"devDependencies": {
|
|
97
|
-
"@sap-ux/ui5-middleware-fe-mockserver": "2.4.17",
|
|
98
|
-
"@types/adm-zip": "0.5.8",
|
|
99
|
-
"@types/cross-spawn": "^6.0.6",
|
|
100
|
-
"@types/diff": "^7.0.2",
|
|
101
|
-
"@types/ejs": "^3.1.5",
|
|
102
|
-
"@types/node": "^20.17.0",
|
|
103
|
-
"@types/react": "^18.3.0",
|
|
104
|
-
"@types/turndown": "^5.0.5",
|
|
105
|
-
"@ui5/builder": "4.3.2",
|
|
106
|
-
"@ui5/fs": "4.0.6",
|
|
107
|
-
"docx": "^9.0.2",
|
|
108
|
-
"husky": "^9.1.7",
|
|
109
|
-
"ink-testing-library": "^4.0.0",
|
|
110
|
-
"pdf-lib": "^1.17.1",
|
|
111
|
-
"strip-ansi": "6.0.1",
|
|
112
|
-
"tsx": "^4.19.0",
|
|
113
|
-
"typescript": "^5.7.0",
|
|
114
|
-
"vitest": "^2.1.0"
|
|
115
|
-
},
|
|
116
|
-
"pnpm": {
|
|
117
|
-
"onlyBuiltDependencies": [
|
|
118
|
-
"esbuild"
|
|
119
|
-
]
|
|
120
|
-
}
|
|
121
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@cspeach/cli",
|
|
3
|
+
"version": "1.1.22",
|
|
4
|
+
"description": "CSPeach — AI-assisted ABAP development CLI. Direct SAP access, 34 skills, safety gates.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"abap",
|
|
7
|
+
"sap",
|
|
8
|
+
"ai",
|
|
9
|
+
"cli",
|
|
10
|
+
"ai-assisted-development",
|
|
11
|
+
"s4hana",
|
|
12
|
+
"abap-development",
|
|
13
|
+
"claude",
|
|
14
|
+
"anthropic"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://cspeach.dev",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"email": "laeeq.siddique@cremencing.com"
|
|
19
|
+
},
|
|
20
|
+
"author": "Laeeq Siddique (Cremencing Solutions)",
|
|
21
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
22
|
+
"private": false,
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"type": "module",
|
|
27
|
+
"bin": {
|
|
28
|
+
"cspeach": "dist/index.js"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"dev": "tsx src/index.ts",
|
|
32
|
+
"prebuild": "tsx scripts/prebuild.ts",
|
|
33
|
+
"build": "tsc && node scripts/make-bin-executable.js",
|
|
34
|
+
"install:global": "tsx scripts/install-global.ts",
|
|
35
|
+
"pretest": "tsx scripts/sync-signing-key.ts || node -e \"console.warn('WARN: sync-signing-key skipped — using stub')\"",
|
|
36
|
+
"start": "node dist/index.js",
|
|
37
|
+
"typecheck": "tsc --noEmit",
|
|
38
|
+
"test": "vitest run",
|
|
39
|
+
"test:watch": "vitest",
|
|
40
|
+
"test:smoke-e2e": "vitest run --config vitest.smoke-e2e.config.ts",
|
|
41
|
+
"adt:smoke": "tsx scripts/adt-smoke.ts",
|
|
42
|
+
"smoke:all": "pnpm zcspeach:smoke && pnpm adt:smoke",
|
|
43
|
+
"zcspeach:smoke": "tsx scripts/zcspeach-smoke.ts",
|
|
44
|
+
"bench:models": "tsx scripts/bench-models.ts",
|
|
45
|
+
"bench:audit": "tsx scripts/bench-audit-task.ts",
|
|
46
|
+
"bench:replay": "tsx bench/replay/run.ts",
|
|
47
|
+
"fiori": "tsx src/tools/fiori/bin.ts",
|
|
48
|
+
"prepare": "husky",
|
|
49
|
+
"prepublishOnly": "node -e \"const deps=require('./package.json').dependencies||{}; for (const n of ['@cspeach/sap-client','@cspeach/register-core']) { const d=deps[n]||''; if(d.startsWith('file:')||d.startsWith('link:')){console.error('publish blocked: '+n+' is a local link ('+d+') — point it at the published version before npm publish'); process.exit(1)} }\""
|
|
50
|
+
},
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=20.0.0"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"@anthropic-ai/sdk": "^0.90.0",
|
|
56
|
+
"@cspeach/register-core": "0.1.0",
|
|
57
|
+
"@cspeach/sap-client": "0.1.15",
|
|
58
|
+
"@iarna/toml": "^2.2.5",
|
|
59
|
+
"@inkjs/ui": "^2.0.0",
|
|
60
|
+
"@inquirer/prompts": "^7.0.0",
|
|
61
|
+
"@napi-rs/keyring": "2.1.0",
|
|
62
|
+
"@noble/ed25519": "2.1.0",
|
|
63
|
+
"@noble/hashes": "1.4.0",
|
|
64
|
+
"@sap-ux/annotation-converter": "0.10.21",
|
|
65
|
+
"@sap-ux/edmx-parser": "0.10.0",
|
|
66
|
+
"@sap-ux/fe-fpm-writer": "0.43.7",
|
|
67
|
+
"@sap-ux/fiori-elements-writer": "^3.0.39",
|
|
68
|
+
"@sap-ux/fiori-freestyle-writer": "3.0.12",
|
|
69
|
+
"@sap-ux/vocabularies-types": "0.15.0",
|
|
70
|
+
"adm-zip": "0.5.17",
|
|
71
|
+
"chalk": "^5.3.0",
|
|
72
|
+
"cli-highlight": "^2.1.11",
|
|
73
|
+
"cli-table3": "^0.6.5",
|
|
74
|
+
"clipboardy": "^5.3.1",
|
|
75
|
+
"cross-spawn": "^7.0.6",
|
|
76
|
+
"diff": "^9.0.0",
|
|
77
|
+
"ejs": "3.1.10",
|
|
78
|
+
"fuse.js": "^7.0.0",
|
|
79
|
+
"ink": "^5.0.0",
|
|
80
|
+
"ink-text-input": "^6.0.0",
|
|
81
|
+
"jose": "^5.9.0",
|
|
82
|
+
"mammoth": "^1.8.0",
|
|
83
|
+
"marked": "^11.2.0",
|
|
84
|
+
"marked-terminal": "^6.2.0",
|
|
85
|
+
"mem-fs": "2.1.0",
|
|
86
|
+
"mem-fs-editor": "9.4.0",
|
|
87
|
+
"node-machine-id": "^1.1.12",
|
|
88
|
+
"openai": "^6.49.0",
|
|
89
|
+
"puppeteer-core": "24.10.2",
|
|
90
|
+
"react": "^18.3.0",
|
|
91
|
+
"string-width": "^7.2.0",
|
|
92
|
+
"turndown": "^7.2.0",
|
|
93
|
+
"unpdf": "1.7.0",
|
|
94
|
+
"zod": "^3.23.0"
|
|
95
|
+
},
|
|
96
|
+
"devDependencies": {
|
|
97
|
+
"@sap-ux/ui5-middleware-fe-mockserver": "2.4.17",
|
|
98
|
+
"@types/adm-zip": "0.5.8",
|
|
99
|
+
"@types/cross-spawn": "^6.0.6",
|
|
100
|
+
"@types/diff": "^7.0.2",
|
|
101
|
+
"@types/ejs": "^3.1.5",
|
|
102
|
+
"@types/node": "^20.17.0",
|
|
103
|
+
"@types/react": "^18.3.0",
|
|
104
|
+
"@types/turndown": "^5.0.5",
|
|
105
|
+
"@ui5/builder": "4.3.2",
|
|
106
|
+
"@ui5/fs": "4.0.6",
|
|
107
|
+
"docx": "^9.0.2",
|
|
108
|
+
"husky": "^9.1.7",
|
|
109
|
+
"ink-testing-library": "^4.0.0",
|
|
110
|
+
"pdf-lib": "^1.17.1",
|
|
111
|
+
"strip-ansi": "6.0.1",
|
|
112
|
+
"tsx": "^4.19.0",
|
|
113
|
+
"typescript": "^5.7.0",
|
|
114
|
+
"vitest": "^2.1.0"
|
|
115
|
+
},
|
|
116
|
+
"pnpm": {
|
|
117
|
+
"onlyBuiltDependencies": [
|
|
118
|
+
"esbuild"
|
|
119
|
+
]
|
|
120
|
+
}
|
|
121
|
+
}
|
package/dist/commands/project.js
DELETED
|
@@ -1,301 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `cspeach project …` — the Project Register from the terminal
|
|
3
|
-
* (spec: docs/superpowers/specs/2026-09-18-project-register-folder-mode.md §6).
|
|
4
|
-
*
|
|
5
|
-
* init <folder> lead: pull the baseline from SAP, write the project folder
|
|
6
|
-
* join <folder> everyone: start recording against a project
|
|
7
|
-
* lane … lead: who owns which packages
|
|
8
|
-
* status [folder] anyone: the register as text
|
|
9
|
-
* outbox what this laptop has recorded, and where
|
|
10
|
-
* rule … lead: settle a disagreement
|
|
11
|
-
* leave stop recording
|
|
12
|
-
*
|
|
13
|
-
* Every path written here is one a person typed. Nothing is written to a
|
|
14
|
-
* network location on CSPeach's own initiative, and none of this is reachable
|
|
15
|
-
* from a model tool.
|
|
16
|
-
*/
|
|
17
|
-
import fs from 'node:fs';
|
|
18
|
-
import path from 'node:path';
|
|
19
|
-
import { buildRegister, objectKey } from '@cspeach/register-core';
|
|
20
|
-
import { LOCAL_PACKAGE, listCustomPackages, pullBaseline, BaselineError } from '../register/baseline.js';
|
|
21
|
-
import { BASELINE_FILE, PROJECT_FILE, RegisterError, activeProject, collectRecords, defaultOutboxDir, loadState, projectIdFor, readBaseline, readLanes, readProjectFile, saveState, sha256, writeLanes, writerIdFor, } from '../register/store.js';
|
|
22
|
-
import { writeRecord } from '../register/records.js';
|
|
23
|
-
const CLASSIFICATIONS = ['keep', 'fix', 'retire', 'redesign'];
|
|
24
|
-
const TYPES = ['clean-core', 'upgrade', 'build'];
|
|
25
|
-
function flags(args) {
|
|
26
|
-
const positional = [];
|
|
27
|
-
const opts = {};
|
|
28
|
-
for (let i = 0; i < args.length; i++) {
|
|
29
|
-
const a = args[i];
|
|
30
|
-
if (a.startsWith('--')) {
|
|
31
|
-
opts[a.slice(2)] = args[i + 1] && !args[i + 1].startsWith('--') ? args[++i] : 'true';
|
|
32
|
-
}
|
|
33
|
-
else
|
|
34
|
-
positional.push(a);
|
|
35
|
-
}
|
|
36
|
-
return { positional, opts };
|
|
37
|
-
}
|
|
38
|
-
const list = (v) => (v ?? '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
39
|
-
/** `ZFI_*` style match, used only to expand --packages patterns against the real package list. */
|
|
40
|
-
function matches(pattern, value) {
|
|
41
|
-
const re = new RegExp(`^${pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*')}$`, 'i');
|
|
42
|
-
return re.test(value);
|
|
43
|
-
}
|
|
44
|
-
export async function runProjectCommand(args, deps) {
|
|
45
|
-
const [sub, ...rest] = args;
|
|
46
|
-
try {
|
|
47
|
-
switch (sub) {
|
|
48
|
-
case 'init': return await init(rest, deps);
|
|
49
|
-
case 'join': return join(rest, deps);
|
|
50
|
-
case 'lane': return lane(rest, deps);
|
|
51
|
-
case 'status': return status(rest, deps);
|
|
52
|
-
case 'outbox': return outbox(rest, deps);
|
|
53
|
-
case 'rule': return rule(rest, deps);
|
|
54
|
-
case 'leave': return leave(deps);
|
|
55
|
-
default:
|
|
56
|
-
deps.print('Usage: cspeach project init|join|lane|status|outbox|rule|leave (cspeach --help for details)');
|
|
57
|
-
return sub ? 1 : 0;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
catch (e) {
|
|
61
|
-
if (e instanceof RegisterError || e instanceof BaselineError) {
|
|
62
|
-
deps.print(`✗ ${e.message}`);
|
|
63
|
-
return 1;
|
|
64
|
-
}
|
|
65
|
-
throw e;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
function requireLead(project, deps) {
|
|
69
|
-
const me = deps.identity().name;
|
|
70
|
-
if (me.trim().toLowerCase() !== project.lead.name.trim().toLowerCase()) {
|
|
71
|
-
throw new RegisterError(`Only the project lead (${project.lead.name}) changes this. You are ${me}.`);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
function joinedOrThrow() {
|
|
75
|
-
const joined = activeProject();
|
|
76
|
-
if (!joined)
|
|
77
|
-
throw new RegisterError('This laptop has not joined a project. Run: cspeach project join <folder>');
|
|
78
|
-
return joined;
|
|
79
|
-
}
|
|
80
|
-
// ── init ────────────────────────────────────────────────────────────────────
|
|
81
|
-
async function init(args, deps) {
|
|
82
|
-
const { positional, opts } = flags(args);
|
|
83
|
-
const folder = positional[0];
|
|
84
|
-
if (!folder)
|
|
85
|
-
throw new RegisterError('Usage: cspeach project init <folder> --name "<name>" [--type clean-core|upgrade|build] [--packages ZFI_*,ZSD_*] [--include-local]');
|
|
86
|
-
const dir = path.resolve(folder);
|
|
87
|
-
if (fs.existsSync(path.join(dir, PROJECT_FILE)))
|
|
88
|
-
throw new RegisterError(`${dir} already holds a project. Pick an empty folder.`);
|
|
89
|
-
const name = opts.name ?? path.basename(dir);
|
|
90
|
-
const type = (opts.type ?? 'clean-core');
|
|
91
|
-
if (!TYPES.includes(type))
|
|
92
|
-
throw new RegisterError(`--type must be one of: ${TYPES.join(', ')}`);
|
|
93
|
-
if (!deps.connect)
|
|
94
|
-
throw new RegisterError('init needs a SAP connection to pull the object list.');
|
|
95
|
-
const { sql, system } = await deps.connect(opts['sap-alias']);
|
|
96
|
-
deps.print('Reading the package list from SAP…');
|
|
97
|
-
const all = await listCustomPackages(sql);
|
|
98
|
-
const patterns = list(opts.packages);
|
|
99
|
-
let chosen = all.filter((p) => patterns.some((pat) => matches(pat, p.package)));
|
|
100
|
-
if (!patterns.length && deps.choosePackages) {
|
|
101
|
-
const picked = new Set(await deps.choosePackages(all));
|
|
102
|
-
chosen = all.filter((p) => picked.has(p.package));
|
|
103
|
-
}
|
|
104
|
-
const local = all.find((p) => p.package === LOCAL_PACKAGE)?.objects ?? 0;
|
|
105
|
-
if (!opts['include-local'])
|
|
106
|
-
chosen = chosen.filter((p) => p.package !== LOCAL_PACKAGE);
|
|
107
|
-
if (!chosen.length) {
|
|
108
|
-
throw new RegisterError(`No package chosen. ${all.length} customer packages exist; name them with --packages, e.g. --packages "ZFI_*,ZSD_*".`);
|
|
109
|
-
}
|
|
110
|
-
deps.print(`Pulling ${chosen.length} package${chosen.length === 1 ? '' : 's'}, checking each against the system's own count…`);
|
|
111
|
-
const pull = await pullBaseline(sql, { packages: chosen });
|
|
112
|
-
if (!pull.objects.length)
|
|
113
|
-
throw new RegisterError('Those packages hold no code objects. Nothing was written.');
|
|
114
|
-
const now = (deps.now ?? (() => new Date()))();
|
|
115
|
-
const projectId = projectIdFor(name);
|
|
116
|
-
const baselineText = `${JSON.stringify({ kind: 'baseline', formatVersion: 1, projectId, objects: pull.objects }, null, 1)}\n`;
|
|
117
|
-
const project = {
|
|
118
|
-
kind: 'project', formatVersion: 1, projectId, name, type, lead: { name: deps.identity().name },
|
|
119
|
-
...(system ? { system } : {}),
|
|
120
|
-
...(opts['atc-variant'] ? { atcVariant: opts['atc-variant'] } : {}),
|
|
121
|
-
baseline: { file: BASELINE_FILE.replace(/\\/g, '/'), sha256: sha256(baselineText), takenAt: now.toISOString(), objectCount: pull.objects.length },
|
|
122
|
-
setAside: { local: opts['include-local'] ? 0 : local, generated: pull.setAside.generated, otherTypes: pull.setAside.otherTypes },
|
|
123
|
-
createdAt: now.toISOString(),
|
|
124
|
-
};
|
|
125
|
-
fs.mkdirSync(path.join(dir, 'baseline'), { recursive: true });
|
|
126
|
-
fs.writeFileSync(path.join(dir, BASELINE_FILE), baselineText);
|
|
127
|
-
fs.writeFileSync(path.join(dir, PROJECT_FILE), `${JSON.stringify(project, null, 2)}\n`);
|
|
128
|
-
writeLanes(dir, projectId, []);
|
|
129
|
-
deps.print(`✓ ${name}: ${pull.objects.length} objects in ${chosen.length} packages. Every package matched the system's count.`);
|
|
130
|
-
deps.print(` Set aside, not in scope: ${project.setAside.local} local ($TMP), ${pull.setAside.generated} generated, ${pull.setAside.otherTypes} non-code entries.`);
|
|
131
|
-
deps.print(` Written to ${dir}`);
|
|
132
|
-
deps.print(' Next: cspeach project lane add <id> --owner "<name>" --packages "ZFI_*" then each person: cspeach project join <folder>');
|
|
133
|
-
return join([dir], deps);
|
|
134
|
-
}
|
|
135
|
-
// ── join / leave ────────────────────────────────────────────────────────────
|
|
136
|
-
function join(args, deps) {
|
|
137
|
-
const { positional, opts } = flags(args);
|
|
138
|
-
if (!positional[0])
|
|
139
|
-
throw new RegisterError('Usage: cspeach project join <project folder> [--outbox <folder>]');
|
|
140
|
-
const projectDir = path.resolve(positional[0]);
|
|
141
|
-
const project = readProjectFile(projectDir);
|
|
142
|
-
const objects = readBaseline(projectDir).length; // also verifies the checksum
|
|
143
|
-
const state = loadState();
|
|
144
|
-
const outboxDir = path.resolve(opts.outbox ?? state.projects[project.projectId]?.outboxDir ?? defaultOutboxDir(project.projectId));
|
|
145
|
-
const writerId = writerIdFor(deps.identity().name);
|
|
146
|
-
fs.mkdirSync(path.join(outboxDir, 'writers', writerId, 'records'), { recursive: true });
|
|
147
|
-
state.projects[project.projectId] = { projectDir, outboxDir, writerId };
|
|
148
|
-
state.active = project.projectId;
|
|
149
|
-
saveState(state);
|
|
150
|
-
deps.print(`✓ Joined "${project.name}" (${objects} objects, lead ${project.lead.name}) as ${writerId}.`);
|
|
151
|
-
deps.print(` Your results are recorded on this laptop, in ${outboxDir}`);
|
|
152
|
-
deps.print(' CSPeach does not send them anywhere. Hand them in by copying that folder\'s "writers" folder into the project folder,');
|
|
153
|
-
deps.print(' or keep the outbox inside your own synced folder: cspeach project join <folder> --outbox <folder>');
|
|
154
|
-
return 0;
|
|
155
|
-
}
|
|
156
|
-
function leave(deps) {
|
|
157
|
-
const state = loadState();
|
|
158
|
-
if (!state.active) {
|
|
159
|
-
deps.print('This laptop is not recording against a project.');
|
|
160
|
-
return 0;
|
|
161
|
-
}
|
|
162
|
-
const was = state.active;
|
|
163
|
-
state.active = null;
|
|
164
|
-
saveState(state);
|
|
165
|
-
deps.print(`✓ Stopped recording against ${was}. Files already written stay where they are.`);
|
|
166
|
-
return 0;
|
|
167
|
-
}
|
|
168
|
-
// ── lane ────────────────────────────────────────────────────────────────────
|
|
169
|
-
function lane(args, deps) {
|
|
170
|
-
const [verb, ...rest] = args;
|
|
171
|
-
const { positional, opts } = flags(rest);
|
|
172
|
-
const joined = joinedOrThrow();
|
|
173
|
-
const project = readProjectFile(joined.projectDir);
|
|
174
|
-
const lanes = readLanes(joined.projectDir);
|
|
175
|
-
if (verb === 'list' || !verb) {
|
|
176
|
-
if (!lanes.length)
|
|
177
|
-
deps.print('No lanes yet. cspeach project lane add <id> --owner "<name>" --packages "ZFI_*"');
|
|
178
|
-
for (const l of lanes)
|
|
179
|
-
deps.print(`${l.id.padEnd(12)} ${l.owner.padEnd(20)} ${[...l.packages, ...l.namePatterns.map((n) => `name:${n}`)].join(', ')}`);
|
|
180
|
-
return 0;
|
|
181
|
-
}
|
|
182
|
-
requireLead(project, deps);
|
|
183
|
-
const id = positional[0];
|
|
184
|
-
if (!id)
|
|
185
|
-
throw new RegisterError('Usage: cspeach project lane add <id> --owner "<name>" --packages "ZFI_*" [--names "ZCL_TAX_*"] | lane remove <id>');
|
|
186
|
-
if (verb === 'remove') {
|
|
187
|
-
writeLanes(joined.projectDir, project.projectId, lanes.filter((l) => l.id !== id));
|
|
188
|
-
deps.print(`✓ Lane ${id} removed.`);
|
|
189
|
-
return 0;
|
|
190
|
-
}
|
|
191
|
-
if (verb !== 'add')
|
|
192
|
-
throw new RegisterError('lane: use add, remove or list.');
|
|
193
|
-
const next = { id, owner: opts.owner ?? '', packages: list(opts.packages), namePatterns: list(opts.names) };
|
|
194
|
-
if (!next.owner || (!next.packages.length && !next.namePatterns.length)) {
|
|
195
|
-
throw new RegisterError('A lane needs --owner and at least one of --packages or --names.');
|
|
196
|
-
}
|
|
197
|
-
writeLanes(joined.projectDir, project.projectId, [...lanes.filter((l) => l.id !== id), next]);
|
|
198
|
-
const reg = registerFor(joined.projectDir, joined.outboxDir);
|
|
199
|
-
const mine = reg.register.lanes.find((l) => l.id === id);
|
|
200
|
-
deps.print(`✓ Lane ${id}: ${next.owner}, ${mine.objects} objects.`);
|
|
201
|
-
if (reg.register.totals.overlapping)
|
|
202
|
-
deps.print(` ! ${reg.register.totals.overlapping} objects now sit in more than one lane.`);
|
|
203
|
-
if (reg.register.totals.unowned)
|
|
204
|
-
deps.print(` ${reg.register.totals.unowned} objects still have no owner.`);
|
|
205
|
-
return 0;
|
|
206
|
-
}
|
|
207
|
-
// ── status ──────────────────────────────────────────────────────────────────
|
|
208
|
-
function registerFor(projectDir, outboxDir) {
|
|
209
|
-
const project = readProjectFile(projectDir);
|
|
210
|
-
const collected = collectRecords(projectDir);
|
|
211
|
-
// This laptop's own outbox counts too, so a developer sees their work before handing it in.
|
|
212
|
-
const own = outboxDir && path.resolve(outboxDir) !== path.resolve(projectDir) ? collectRecords(outboxDir) : { records: [], unreadable: 0, files: 0 };
|
|
213
|
-
const register = buildRegister({
|
|
214
|
-
project, baseline: readBaseline(projectDir), lanes: readLanes(projectDir),
|
|
215
|
-
records: [...collected.records, ...own.records],
|
|
216
|
-
});
|
|
217
|
-
return { register, files: collected.files + own.files, unreadable: collected.unreadable + own.unreadable };
|
|
218
|
-
}
|
|
219
|
-
const STATE_WORDS = [
|
|
220
|
-
['verified', 'verified'], ['clean', 'clean'], ['claimed', 'claimed, not measured since'], ['working', 'in work'],
|
|
221
|
-
['decided', 'decided, not started'], ['untouched', 'untouched'], ['did-not-hold', 'fix did not hold'],
|
|
222
|
-
['regressed', 'regressed'], ['retired', 'retired'],
|
|
223
|
-
];
|
|
224
|
-
function status(args, deps) {
|
|
225
|
-
const { positional } = flags(args);
|
|
226
|
-
const joined = activeProject();
|
|
227
|
-
const projectDir = positional[0] ? path.resolve(positional[0]) : joined?.projectDir;
|
|
228
|
-
if (!projectDir)
|
|
229
|
-
throw new RegisterError('Usage: cspeach project status [<project folder>]');
|
|
230
|
-
const project = readProjectFile(projectDir);
|
|
231
|
-
const { register: r, files, unreadable } = registerFor(projectDir, positional[0] ? undefined : joined?.outboxDir);
|
|
232
|
-
const t = r.totals;
|
|
233
|
-
deps.print(`${project.name} (${project.type}, lead ${project.lead.name})`);
|
|
234
|
-
deps.print(`${t.objects} objects, ${t.active} active. ${t.decided} decided. ${t.byState.verified} verified, ${t.byState.clean} clean.`);
|
|
235
|
-
deps.print(r.lastMeasuredAt ? `Last measured ${r.lastMeasuredAt.slice(0, 16).replace('T', ' ')} UTC.` : 'Not measured yet: "verified" and "clean" need an ATC run.');
|
|
236
|
-
deps.print('');
|
|
237
|
-
for (const [state, word] of STATE_WORDS)
|
|
238
|
-
if (t.byState[state])
|
|
239
|
-
deps.print(` ${String(t.byState[state]).padStart(6)} ${word}`);
|
|
240
|
-
deps.print('');
|
|
241
|
-
if (r.lanes.length) {
|
|
242
|
-
deps.print('Lane Owner Objects Done Last record');
|
|
243
|
-
for (const l of r.lanes) {
|
|
244
|
-
const done = l.byState.verified + l.byState.clean + l.byState.retired;
|
|
245
|
-
deps.print(`${l.id.padEnd(12)} ${l.owner.padEnd(20)} ${String(l.objects).padStart(7)} ${String(done).padStart(5)} ${l.lastRecordAt ? l.lastRecordAt.slice(0, 10) : 'nothing yet'}`);
|
|
246
|
-
}
|
|
247
|
-
deps.print('');
|
|
248
|
-
}
|
|
249
|
-
if (t.unowned)
|
|
250
|
-
deps.print(`! ${t.unowned} objects have no owner.`);
|
|
251
|
-
if (t.overlapping)
|
|
252
|
-
deps.print(`! ${t.overlapping} objects sit in more than one lane.`);
|
|
253
|
-
if (t.conflicts) {
|
|
254
|
-
deps.print(`! ${t.conflicts} objects have conflicting decisions. The lead settles each with: cspeach project rule <TYPE> <NAME> <decision>`);
|
|
255
|
-
for (const o of r.objects.filter((x) => x.decision?.conflict).slice(0, 10)) {
|
|
256
|
-
deps.print(` ${o.key}: ${Object.entries(o.decision.conflict).map(([w, c]) => `${w} says ${c}`).join(', ')}`);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
const s = r.skipped;
|
|
260
|
-
if (unreadable)
|
|
261
|
-
deps.print(` ${unreadable} of ${files} files could not be read yet (still syncing, or damaged).`);
|
|
262
|
-
if (s.rowsNotInBaseline)
|
|
263
|
-
deps.print(` ${s.rowsNotInBaseline} rows named objects that are not in this project: ${r.unknownKeys.slice(0, 5).join(', ')}`);
|
|
264
|
-
if (s.otherProject)
|
|
265
|
-
deps.print(` ${s.otherProject} files belong to another project and were ignored.`);
|
|
266
|
-
if (s.rulingsNotFromLead)
|
|
267
|
-
deps.print(` ${s.rulingsNotFromLead} rulings were not from the lead and were ignored.`);
|
|
268
|
-
return 0;
|
|
269
|
-
}
|
|
270
|
-
// ── outbox / rule ───────────────────────────────────────────────────────────
|
|
271
|
-
function outbox(_args, deps) {
|
|
272
|
-
const joined = joinedOrThrow();
|
|
273
|
-
const dir = path.join(joined.outboxDir, 'writers', joined.writerId, 'records');
|
|
274
|
-
const files = fs.existsSync(dir) ? fs.readdirSync(dir).filter((f) => f.endsWith('.json')).sort() : [];
|
|
275
|
-
deps.print(`Records this laptop has written for ${joined.projectId}: ${files.length}`);
|
|
276
|
-
deps.print(` ${dir}`);
|
|
277
|
-
for (const f of files.slice(-15)) {
|
|
278
|
-
let rows = '?';
|
|
279
|
-
try {
|
|
280
|
-
rows = String(JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8')).rows.length);
|
|
281
|
-
}
|
|
282
|
-
catch { /* listed anyway */ }
|
|
283
|
-
deps.print(` ${f} (${rows} rows)`);
|
|
284
|
-
}
|
|
285
|
-
deps.print('Each file is plain JSON: object names, decisions, statuses. No source code. Open one in Notepad to check.');
|
|
286
|
-
return 0;
|
|
287
|
-
}
|
|
288
|
-
function rule(args, deps) {
|
|
289
|
-
const { positional, opts } = flags(args);
|
|
290
|
-
const [type, name, classification] = positional;
|
|
291
|
-
if (!type || !name || !CLASSIFICATIONS.includes(classification)) {
|
|
292
|
-
throw new RegisterError(`Usage: cspeach project rule <TYPE> <NAME> ${CLASSIFICATIONS.join('|')} [--reason "<why>"]`);
|
|
293
|
-
}
|
|
294
|
-
const joined = joinedOrThrow();
|
|
295
|
-
requireLead(readProjectFile(joined.projectDir), deps);
|
|
296
|
-
const res = writeRecord(joined, 'ruling', [{ key: objectKey(type, name), classification, ...(opts.reason ? { reason: opts.reason } : {}) }], deps.identity(), { dir: path.join(joined.projectDir, 'rulings'), now: deps.now?.() });
|
|
297
|
-
if (!res.file)
|
|
298
|
-
throw new RegisterError(`${objectKey(type, name)} is not in this project's baseline.`);
|
|
299
|
-
deps.print(`✓ Ruled ${objectKey(type, name)} = ${classification}.`);
|
|
300
|
-
return 0;
|
|
301
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
// cspeach-cli/src/tools/local-files.ts
|
|
2
|
-
/**
|
|
3
|
-
* Startup wiring for the persisted `local_files` config toggle.
|
|
4
|
-
*
|
|
5
|
-
* When `cfg.local_files === true`, the otherwise flag-gated filesystem tools
|
|
6
|
-
* (file_read/file_write/file_edit/glob/grep) are force-enabled via the
|
|
7
|
-
* in-memory override in `_flag.ts`, BEFORE the first listTools()/turn. This is
|
|
8
|
-
* what makes /abap-fiori-build runnable — it writes local app files.
|
|
9
|
-
*
|
|
10
|
-
* The toggle is PERSISTED (config.toml), not re-consented each session, so
|
|
11
|
-
* `applyLocalFilesConfig` returns an announce line the entry point prints
|
|
12
|
-
* whenever the tools are live — the user must always see that local file
|
|
13
|
-
* access is on this session (trust-visibility requirement). The FS tools'
|
|
14
|
-
* own cwd-sandbox (writes confined to the project root; .cspeach/.env/.git
|
|
15
|
-
* denylisted) still applies regardless of this toggle.
|
|
16
|
-
*/
|
|
17
|
-
import { enableToolFlags } from './_flag.js';
|
|
18
|
-
/** The exact tool NAMEs the flag uses (file_read → CSPEACH_TOOL_FILE_READ). */
|
|
19
|
-
export const LOCAL_FILE_TOOLS = ['file_read', 'file_write', 'file_edit', 'glob', 'grep'];
|
|
20
|
-
/**
|
|
21
|
-
* Apply the persisted `local_files` toggle. When on, force-enables the FS
|
|
22
|
-
* tools and returns a one-line announce string (undefined when off, so the
|
|
23
|
-
* caller prints nothing). `cwd` is surfaced so the user sees exactly which
|
|
24
|
-
* directory tree read/write/edit operate under.
|
|
25
|
-
*/
|
|
26
|
-
export function applyLocalFilesConfig(localFiles, cwd) {
|
|
27
|
-
if (localFiles !== true)
|
|
28
|
-
return undefined;
|
|
29
|
-
enableToolFlags([...LOCAL_FILE_TOOLS]);
|
|
30
|
-
return `local file tools enabled (read/write/edit under ${cwd}) — cspeach config set local_files off to disable`;
|
|
31
|
-
}
|