abapgit-agent 1.12.1 → 1.13.1
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 +30 -133
- package/abap/.github/copilot-instructions.slim.md +114 -0
- package/abap/CLAUDE.md +66 -21
- package/abap/CLAUDE.slim.md +42 -0
- package/abap/guidelines/abapgit.md +9 -47
- package/abap/guidelines/cds-testing.md +4 -1
- package/abap/guidelines/cds.md +38 -10
- package/abap/guidelines/testing.md +46 -2
- package/bin/abapgit-agent +1 -0
- package/package.json +3 -1
- package/src/commands/guide.js +207 -0
- package/src/commands/help.js +15 -0
- package/src/commands/init.js +103 -64
- package/src/commands/pull.js +13 -2
- package/src/utils/abap-reference.js +254 -118
package/abap/guidelines/cds.md
CHANGED
|
@@ -8,7 +8,7 @@ grand_parent: ABAP Development
|
|
|
8
8
|
|
|
9
9
|
# Creating CDS Views
|
|
10
10
|
|
|
11
|
-
**Searchable keywords**: CDS, DDL, DDLS, CDS view, @AbapCatalog, @AccessControl, association, projection, consumption
|
|
11
|
+
**Searchable keywords**: CDS, DDL, DDLS, CDS view, @AbapCatalog, @AccessControl, association, projection, consumption, GROUP BY, aggregation, subquery, JOIN
|
|
12
12
|
|
|
13
13
|
## TOPICS IN THIS FILE
|
|
14
14
|
1. File Naming - line 96
|
|
@@ -145,20 +145,13 @@ where devclass not like '$%'
|
|
|
145
145
|
|
|
146
146
|
1. **Avoid reserved words** - Field names like `PACKAGE`, `CLASS`, `INTERFACE` are reserved in CDS. Use alternatives like `PackageName`, `ClassName`.
|
|
147
147
|
|
|
148
|
-
2. **Workflow for creating CDS views**
|
|
149
|
-
- Independent CDS views: `syntax → commit → pull --files`
|
|
150
|
-
- Dependent CDS views (with associations to NEW views): Create underlying view first, then dependent view
|
|
151
|
-
- See CLAUDE.md section on "Working with dependent objects"
|
|
148
|
+
2. **Workflow for creating CDS views** — run: `abapgit-agent ref --topic workflow-detailed`
|
|
152
149
|
|
|
153
150
|
3. **System support** - CDS views require SAP systems with CDS capability (S/4HANA, SAP BW/4HANA, or ABAP 7.51+). Older systems will show error: "Object type DDLS is not supported by this system"
|
|
154
151
|
|
|
155
152
|
### Activating CDS Views
|
|
156
153
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
**CDS-specific notes:**
|
|
160
|
-
- Single independent DDLS file: `abapgit-agent pull --files src/zc_view.ddls.asddls`
|
|
161
|
-
- CDS views with associations to OTHER NEW views: Create target view first (see `../CLAUDE.md` for workflow)
|
|
154
|
+
→ See `abapgit-agent ref --topic workflow-detailed` for the complete workflow.
|
|
162
155
|
|
|
163
156
|
## CDS View Entity Features
|
|
164
157
|
|
|
@@ -196,6 +189,39 @@ where devclass not like '$%'
|
|
|
196
189
|
|
|
197
190
|
## CDS Best Practices and Common Patterns
|
|
198
191
|
|
|
192
|
+
### No Inline Subqueries — Use JOIN + GROUP BY Instead
|
|
193
|
+
|
|
194
|
+
**CDS view entities do NOT support inline subqueries in JOIN.** This is a hard syntax error:
|
|
195
|
+
|
|
196
|
+
❌ **WRONG — inline subquery (syntax error)**:
|
|
197
|
+
```abap
|
|
198
|
+
define view entity ZC_MyView as select from zheader as Header
|
|
199
|
+
inner join ( // ← NOT SUPPORTED
|
|
200
|
+
select docid, sum( amount ) as Total
|
|
201
|
+
from zitems
|
|
202
|
+
group by docid
|
|
203
|
+
) as Items on Items.docid = Header.docid ...
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
✅ **CORRECT — JOIN base tables directly, use GROUP BY on the outer view**:
|
|
207
|
+
```abap
|
|
208
|
+
define view entity ZC_MyView
|
|
209
|
+
as select from zheader as Header
|
|
210
|
+
inner join zitems as Item
|
|
211
|
+
on Item.docid = Header.docid
|
|
212
|
+
{
|
|
213
|
+
key Header.docid as DocId,
|
|
214
|
+
Header.description as Description,
|
|
215
|
+
count(*) as NumberOfItems,
|
|
216
|
+
sum(Item.amount) as TotalAmount
|
|
217
|
+
}
|
|
218
|
+
group by Header.docid, Header.description
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**This works.** CDS supports JOIN + GROUP BY + aggregation functions in a single view. Only inline subqueries in the FROM/JOIN clause are unsupported.
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
199
225
|
### Key Field Ordering (STRICT RULE)
|
|
200
226
|
|
|
201
227
|
CDS views enforce strict key field ordering that differs from regular SQL:
|
|
@@ -235,6 +261,8 @@ CDS views enforce strict key field ordering that differs from regular SQL:
|
|
|
235
261
|
|
|
236
262
|
### Currency/Amount Field Aggregation
|
|
237
263
|
|
|
264
|
+
> **Design principle**: Prefer a **single CDS view** with JOIN + GROUP BY. Only split into multiple layered views when requirements specifically need reusable intermediate aggregations shared across different consumers.
|
|
265
|
+
|
|
238
266
|
When aggregating currency or amount fields in CDS views, use semantic annotations instead of complex casting:
|
|
239
267
|
|
|
240
268
|
❌ **WRONG - Complex casting (will fail)**:
|
|
@@ -290,6 +290,49 @@ METHOD test_aggregation.
|
|
|
290
290
|
ENDMETHOD.
|
|
291
291
|
```
|
|
292
292
|
|
|
293
|
+
### Testing CDS Views that Select from Another CDS View
|
|
294
|
+
|
|
295
|
+
> **Note:** This pattern applies when your design **already has** a CDS view that selects from another CDS view. It does NOT mean you should split a single view into two — use a single CDS view with GROUP BY / JOIN when the business logic fits.
|
|
296
|
+
|
|
297
|
+
When your CDS view selects from **another CDS view** (not a base table), `create` will raise `CX_CDS_FAILURE`. Use `create_for_multiple_cds` instead and list all CDS entities in the dependency chain.
|
|
298
|
+
|
|
299
|
+
```abap
|
|
300
|
+
METHOD class_setup.
|
|
301
|
+
" ZC_TopView selects from ZC_IntermediateView (another CDS view entity)
|
|
302
|
+
" → must use create_for_multiple_cds and list all CDS entities
|
|
303
|
+
mo_cds_env_static = cl_cds_test_environment=>create_for_multiple_cds(
|
|
304
|
+
i_for_entities = VALUE #(
|
|
305
|
+
( 'ZC_TOPVIEW' ) " the view under test
|
|
306
|
+
( 'ZC_INTERMEDIATEVIEW' ) " the CDS view it selects from
|
|
307
|
+
) ).
|
|
308
|
+
ENDMETHOD.
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Insert test data into the intermediate CDS view (not the base tables), since that is what the top-level view reads:
|
|
312
|
+
|
|
313
|
+
```abap
|
|
314
|
+
METHOD test_read.
|
|
315
|
+
DATA lt_source TYPE TABLE OF zc_intermediateview WITH EMPTY KEY.
|
|
316
|
+
lt_source = VALUE #(
|
|
317
|
+
( field1 = 'A' field2 = 100 )
|
|
318
|
+
( field1 = 'B' field2 = 200 ) ).
|
|
319
|
+
mo_cds_env->insert_test_data( i_data = lt_source ).
|
|
320
|
+
|
|
321
|
+
SELECT * FROM zc_topview INTO TABLE @DATA(lt_result).
|
|
322
|
+
|
|
323
|
+
cl_abap_unit_assert=>assert_equals(
|
|
324
|
+
exp = 2 act = lines( lt_result ) msg = 'Expected 2 rows' ).
|
|
325
|
+
ENDMETHOD.
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
**Rules:**
|
|
329
|
+
- List the view under test **and all CDS views it depends on** in `i_for_entities`
|
|
330
|
+
- Insert data into the **direct source** of the top-level view (the intermediate CDS view)
|
|
331
|
+
- Order in `i_for_entities` does not matter
|
|
332
|
+
- If you get `CX_CDS_FAILURE` when using `create`, switch to `create_for_multiple_cds`
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
293
336
|
### Key Classes for CDS Testing
|
|
294
337
|
|
|
295
338
|
| Item | Type/Usage |
|
|
@@ -304,7 +347,8 @@ ENDMETHOD.
|
|
|
304
347
|
|
|
305
348
|
| Method | Purpose |
|
|
306
349
|
|--------|---------|
|
|
307
|
-
| `CL_CDS_TEST_ENVIRONMENT=>create( i_for_entity = ... )` | Create test environment
|
|
350
|
+
| `CL_CDS_TEST_ENVIRONMENT=>create( i_for_entity = ... )` | Create test environment for a CDS view over base tables |
|
|
351
|
+
| `CL_CDS_TEST_ENVIRONMENT=>create_for_multiple_cds( i_for_entities = ... )` | Create test environment when the CDS view selects from another CDS view |
|
|
308
352
|
| `insert_test_data( i_data = ... )` | Insert test data into test doubles |
|
|
309
353
|
| `clear_doubles` | Clear test data before each test method |
|
|
310
354
|
| `destroy` | Clean up after test class |
|
|
@@ -314,7 +358,7 @@ ENDMETHOD.
|
|
|
314
358
|
1. **Use interface type**: `DATA mo_cds_env TYPE REF TO if_cds_test_environment` - the CREATE method returns an interface reference
|
|
315
359
|
2. **CLASS-METHODS required**: `class_setup` and `class_teardown` must be declared with `CLASS-METHODS` (not `METHODS`)
|
|
316
360
|
3. **Table type declaration**: Must declare `DATA lt_tab TYPE TABLE OF <type> WITH EMPTY KEY` before using `VALUE #()`
|
|
317
|
-
4. **Auto-created dependencies**: CDS framework auto-creates test doubles
|
|
361
|
+
4. **Auto-created dependencies**: When the CDS view selects only from **base tables**, the framework auto-creates test doubles — do not specify `i_dependency_list`. When the CDS view selects from **another CDS view**, use `create_for_multiple_cds` instead (see section below).
|
|
318
362
|
5. **Aggregations**: For CDS views with SUM/COUNT/GROUP BY, insert test data into base tables (SFLIGHT, SCARR, etc.)
|
|
319
363
|
6. **Clear doubles**: Call `clear_doubles` in `setup` method before each test
|
|
320
364
|
7. **Enable associations**: Set `test_associations = 'X'` only if testing CDS associations
|
package/bin/abapgit-agent
CHANGED
|
@@ -54,6 +54,7 @@ async function main() {
|
|
|
54
54
|
debug: require('../src/commands/debug'),
|
|
55
55
|
run: require('../src/commands/run'),
|
|
56
56
|
ref: require('../src/commands/ref'),
|
|
57
|
+
guide: require('../src/commands/guide'),
|
|
57
58
|
init: require('../src/commands/init'),
|
|
58
59
|
pull: require('../src/commands/pull'),
|
|
59
60
|
upgrade: require('../src/commands/upgrade'),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "abapgit-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.1",
|
|
4
4
|
"description": "ABAP Git Agent - Pull and activate ABAP code via abapGit from any git repository",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
"src/",
|
|
9
9
|
"abap/guidelines/",
|
|
10
10
|
"abap/CLAUDE.md",
|
|
11
|
+
"abap/CLAUDE.slim.md",
|
|
11
12
|
"abap/.github/copilot-instructions.md",
|
|
13
|
+
"abap/.github/copilot-instructions.slim.md",
|
|
12
14
|
".abapGitAgent.example"
|
|
13
15
|
],
|
|
14
16
|
"bin": {
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const readline = require('readline');
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
name: 'guide',
|
|
9
|
+
description: 'Show bundled ABAP development guide',
|
|
10
|
+
requiresAbapConfig: false,
|
|
11
|
+
|
|
12
|
+
_findBundledGuide() {
|
|
13
|
+
const candidates = [
|
|
14
|
+
path.join(__dirname, '..', '..', 'abap', 'CLAUDE.md'),
|
|
15
|
+
path.join(__dirname, '..', '..', '..', 'abap', 'CLAUDE.md')
|
|
16
|
+
];
|
|
17
|
+
return candidates.find(p => fs.existsSync(p)) || null;
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
_findSlimStub() {
|
|
21
|
+
const candidates = [
|
|
22
|
+
path.join(__dirname, '..', '..', 'abap', 'CLAUDE.slim.md'),
|
|
23
|
+
path.join(__dirname, '..', '..', '..', 'abap', 'CLAUDE.slim.md')
|
|
24
|
+
];
|
|
25
|
+
return candidates.find(p => fs.existsSync(p)) || null;
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
_findCopilotSlimStub() {
|
|
29
|
+
const candidates = [
|
|
30
|
+
path.join(__dirname, '..', '..', 'abap', '.github', 'copilot-instructions.slim.md'),
|
|
31
|
+
path.join(__dirname, '..', '..', '..', 'abap', '.github', 'copilot-instructions.slim.md')
|
|
32
|
+
];
|
|
33
|
+
return candidates.find(p => fs.existsSync(p)) || null;
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
async _confirm(question) {
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
39
|
+
rl.question(question, (answer) => {
|
|
40
|
+
rl.close();
|
|
41
|
+
const normalized = answer.trim().toLowerCase();
|
|
42
|
+
resolve(normalized === '' || normalized === 'y' || normalized === 'yes');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
async _runMigrate(args) {
|
|
48
|
+
const dryRun = args.includes('--dry-run');
|
|
49
|
+
const yes = args.includes('--yes') || args.includes('-y');
|
|
50
|
+
const cwd = process.cwd();
|
|
51
|
+
|
|
52
|
+
const slimStubPath = this._findSlimStub();
|
|
53
|
+
const copilotSlimStubPath = this._findCopilotSlimStub();
|
|
54
|
+
|
|
55
|
+
// --- Scan guidelines/: delete all *.md except *.local.md ---
|
|
56
|
+
const guidelinesDir = path.join(cwd, 'guidelines');
|
|
57
|
+
const toDelete = [];
|
|
58
|
+
const toKeep = [];
|
|
59
|
+
|
|
60
|
+
if (fs.existsSync(guidelinesDir)) {
|
|
61
|
+
for (const name of fs.readdirSync(guidelinesDir)) {
|
|
62
|
+
if (!name.endsWith('.md')) continue;
|
|
63
|
+
if (name.endsWith('.local.md')) {
|
|
64
|
+
toKeep.push(name);
|
|
65
|
+
} else {
|
|
66
|
+
toDelete.push(path.join(guidelinesDir, name));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// --- CLAUDE.md: replace if it exists ---
|
|
72
|
+
const claudeMdPath = path.join(cwd, 'CLAUDE.md');
|
|
73
|
+
const claudeExists = fs.existsSync(claudeMdPath);
|
|
74
|
+
|
|
75
|
+
// --- .github/copilot-instructions.md: replace if it exists ---
|
|
76
|
+
const copilotMdPath = path.join(cwd, '.github', 'copilot-instructions.md');
|
|
77
|
+
const copilotExists = fs.existsSync(copilotMdPath);
|
|
78
|
+
|
|
79
|
+
// --- Nothing to do? ---
|
|
80
|
+
const nothingToDo = toDelete.length === 0 && !claudeExists && !copilotExists;
|
|
81
|
+
if (nothingToDo) {
|
|
82
|
+
console.log('');
|
|
83
|
+
console.log('✅ Nothing to migrate — no guideline files, CLAUDE.md, or copilot-instructions.md found.');
|
|
84
|
+
console.log('');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// --- Preview ---
|
|
89
|
+
console.log('');
|
|
90
|
+
console.log('🔄 guide --migrate: switch to bundled guidelines');
|
|
91
|
+
console.log('');
|
|
92
|
+
|
|
93
|
+
if (toDelete.length > 0) {
|
|
94
|
+
console.log(`Files to remove (${toDelete.length} guideline file${toDelete.length > 1 ? 's' : ''}):`);
|
|
95
|
+
toDelete.forEach(f => console.log(` ${path.relative(cwd, f)}`));
|
|
96
|
+
console.log('');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (toKeep.length > 0) {
|
|
100
|
+
console.log('Files to keep (project-specific):');
|
|
101
|
+
toKeep.forEach(f => console.log(` guidelines/${f}`));
|
|
102
|
+
console.log('');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (claudeExists) {
|
|
106
|
+
if (slimStubPath) {
|
|
107
|
+
console.log('CLAUDE.md → will replace with slim stub');
|
|
108
|
+
console.log(" (run 'abapgit-agent guide' to read the full guide on demand)");
|
|
109
|
+
} else {
|
|
110
|
+
console.log('CLAUDE.md → ⚠️ slim stub not found, will skip');
|
|
111
|
+
}
|
|
112
|
+
console.log('');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (copilotExists) {
|
|
116
|
+
if (copilotSlimStubPath) {
|
|
117
|
+
console.log('.github/copilot-instructions.md → will replace with slim stub');
|
|
118
|
+
} else {
|
|
119
|
+
console.log('.github/copilot-instructions.md → ⚠️ slim stub not found, will skip');
|
|
120
|
+
}
|
|
121
|
+
console.log('');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const dirWillBeEmpty = fs.existsSync(guidelinesDir) && toKeep.length === 0;
|
|
125
|
+
if (dirWillBeEmpty) {
|
|
126
|
+
console.log('guidelines/ will be removed (no project-specific files remain).');
|
|
127
|
+
console.log('');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (dryRun) {
|
|
131
|
+
console.log('ℹ️ Dry run — no changes made.');
|
|
132
|
+
console.log('');
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// --- Confirm ---
|
|
137
|
+
if (!yes) {
|
|
138
|
+
const proceed = await this._confirm('Proceed? [y/N] ');
|
|
139
|
+
if (!proceed) {
|
|
140
|
+
console.log('Migration cancelled.');
|
|
141
|
+
console.log('');
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// --- Execute ---
|
|
147
|
+
console.log('');
|
|
148
|
+
|
|
149
|
+
for (const filePath of toDelete) {
|
|
150
|
+
fs.unlinkSync(filePath);
|
|
151
|
+
console.log(`🗑️ Removed ${path.relative(cwd, filePath)}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (dirWillBeEmpty) {
|
|
155
|
+
const remaining = fs.readdirSync(guidelinesDir);
|
|
156
|
+
if (remaining.length === 0) {
|
|
157
|
+
fs.rmdirSync(guidelinesDir);
|
|
158
|
+
console.log('🗑️ Removed guidelines/');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (claudeExists && slimStubPath) {
|
|
163
|
+
const slimContent = fs.readFileSync(slimStubPath, 'utf8');
|
|
164
|
+
fs.writeFileSync(claudeMdPath, slimContent);
|
|
165
|
+
console.log('✅ Replaced CLAUDE.md with slim stub');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (copilotExists && copilotSlimStubPath) {
|
|
169
|
+
const slimContent = fs.readFileSync(copilotSlimStubPath, 'utf8');
|
|
170
|
+
fs.writeFileSync(copilotMdPath, slimContent);
|
|
171
|
+
console.log('✅ Replaced .github/copilot-instructions.md with slim stub');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
console.log('');
|
|
175
|
+
console.log('✅ Migration complete.');
|
|
176
|
+
console.log(' Standard guidelines are now read from the package automatically.');
|
|
177
|
+
console.log(" Run 'abapgit-agent ref \"<pattern>\"' or 'abapgit-agent ref --topic <topic>' to search them.");
|
|
178
|
+
console.log('');
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
async execute(args) {
|
|
182
|
+
if (args.includes('--migrate')) {
|
|
183
|
+
return this._runMigrate(args);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const filePath = this._findBundledGuide();
|
|
187
|
+
|
|
188
|
+
if (!filePath) {
|
|
189
|
+
console.error('❌ Bundled CLAUDE.md not found. Make sure abapgit-agent is properly installed.');
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (args.includes('--path')) {
|
|
194
|
+
console.log(filePath);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
199
|
+
|
|
200
|
+
if (args.includes('--json')) {
|
|
201
|
+
console.log(JSON.stringify({ path: filePath, content }));
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
console.log(content);
|
|
206
|
+
}
|
|
207
|
+
};
|
package/src/commands/help.js
CHANGED
|
@@ -78,6 +78,19 @@ Commands:
|
|
|
78
78
|
- Use full URL: https://github.com/user/repo.git
|
|
79
79
|
- Or short name: user/repo or user/repo (assumes github.com)
|
|
80
80
|
|
|
81
|
+
guide [--path] [--json]
|
|
82
|
+
Show the full ABAP development guide bundled with the package.
|
|
83
|
+
Reads directly from the installed npm package — always up-to-date.
|
|
84
|
+
- Use --path to print only the file path (useful for pagers)
|
|
85
|
+
- Use --json for machine-readable output
|
|
86
|
+
|
|
87
|
+
guide --migrate [--dry-run] [--yes]
|
|
88
|
+
Migrate repo from locally-copied guidelines to bundled fallback.
|
|
89
|
+
Removes standard guideline files copied by old 'init', replaces full CLAUDE.md
|
|
90
|
+
with slim stub. Project-specific files (*.local.md) are never removed.
|
|
91
|
+
- Use --dry-run to preview changes without applying them
|
|
92
|
+
- Use --yes to skip confirmation prompt
|
|
93
|
+
|
|
81
94
|
health
|
|
82
95
|
Check if ABAP REST API is healthy
|
|
83
96
|
|
|
@@ -107,6 +120,8 @@ Examples:
|
|
|
107
120
|
abapgit-agent dump --user DEVELOPER --detail 1 # Full detail of first result
|
|
108
121
|
abapgit-agent ref "CORRESPONDING" # Search for pattern
|
|
109
122
|
abapgit-agent ref --topic exceptions # View exceptions topic
|
|
123
|
+
abapgit-agent guide # Full ABAP dev guide
|
|
124
|
+
abapgit-agent guide --path # Path to guide file
|
|
110
125
|
abapgit-agent health # Health check
|
|
111
126
|
abapgit-agent status # Configuration status
|
|
112
127
|
|
package/src/commands/init.js
CHANGED
|
@@ -202,34 +202,82 @@ Examples:
|
|
|
202
202
|
console.log(`\n🔄 Updating abapGit Agent files`);
|
|
203
203
|
console.log('');
|
|
204
204
|
|
|
205
|
-
//
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
205
|
+
// copilot-instructions.md: never overwrite — user may have customised it
|
|
206
|
+
const localCopilotMdPath = pathModule.join(process.cwd(), '.github', 'copilot-instructions.md');
|
|
207
|
+
if (!fs.existsSync(localCopilotMdPath)) {
|
|
208
|
+
await copyFileIfExists(
|
|
209
|
+
pathModule.join(__dirname, '..', '..', 'abap', '.github', 'copilot-instructions.slim.md'),
|
|
210
|
+
localCopilotMdPath,
|
|
211
|
+
'.github/copilot-instructions.md',
|
|
212
|
+
true // create parent dir
|
|
213
|
+
);
|
|
214
|
+
} else {
|
|
215
|
+
console.log(`⚠️ .github/copilot-instructions.md already exists, skipped`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// CLAUDE.md: never overwrite — user may have customised it
|
|
219
|
+
const localClaudeMdPath = pathModule.join(process.cwd(), 'CLAUDE.md');
|
|
220
|
+
if (!fs.existsSync(localClaudeMdPath)) {
|
|
221
|
+
await copyFileIfExists(
|
|
222
|
+
pathModule.join(__dirname, '..', '..', 'abap', 'CLAUDE.slim.md'),
|
|
223
|
+
localClaudeMdPath,
|
|
224
|
+
'CLAUDE.md'
|
|
225
|
+
);
|
|
226
|
+
} else {
|
|
227
|
+
console.log(`⚠️ CLAUDE.md already exists, skipped (use 'abapgit-agent guide --migrate' to replace a full guide with the slim stub)`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// guidelines/: never copy standard files — they're bundled in the package now.
|
|
231
|
+
// Only ensure objects.local.md stub exists if missing.
|
|
232
|
+
const guidelinesDestPath = pathModule.join(process.cwd(), 'guidelines');
|
|
233
|
+
if (!fs.existsSync(guidelinesDestPath)) {
|
|
234
|
+
fs.mkdirSync(guidelinesDestPath, { recursive: true });
|
|
235
|
+
console.log(`✅ Created guidelines/`);
|
|
236
|
+
}
|
|
237
|
+
const localNamingPath = pathModule.join(guidelinesDestPath, 'objects.local.md');
|
|
238
|
+
if (!fs.existsSync(localNamingPath)) {
|
|
239
|
+
// reuse the same stub content as init
|
|
240
|
+
const localNamingStub = `---
|
|
241
|
+
nav_order: 8
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
# Project Naming Conventions (Override)
|
|
245
|
+
|
|
246
|
+
This file overrides \`guidelines/objects.md\` for this project.
|
|
247
|
+
It is **never overwritten** by \`abapgit-agent init --update\` — safe to customise.
|
|
248
|
+
|
|
249
|
+
Searched by the \`ref\` command alongside all other guidelines.
|
|
250
|
+
|
|
251
|
+
## Naming Conventions
|
|
252
|
+
|
|
253
|
+
Uncomment and edit the rows that differ from the defaults in \`guidelines/objects.md\`:
|
|
254
|
+
|
|
255
|
+
| Object Type | Prefix | Example |
|
|
256
|
+
|---|---|---|
|
|
257
|
+
| Class | ZCL_ | ZCL_MY_CLASS |
|
|
258
|
+
| Interface | ZIF_ | ZIF_MY_INTERFACE |
|
|
259
|
+
| Program | Z | ZMY_PROGRAM |
|
|
260
|
+
| Package | $ | $MY_PACKAGE |
|
|
261
|
+
| Table | Z | ZMY_TABLE |
|
|
262
|
+
| CDS View | ZC_ | ZC_MY_VIEW |
|
|
263
|
+
| CDS Entity | ZE_ | ZE_MY_ENTITY |
|
|
264
|
+
| Data Element | Z | ZMY_ELEMENT |
|
|
265
|
+
| Structure | Z | ZMY_STRUCTURE |
|
|
266
|
+
| Table Type | Z | ZMY_TABLE_TYPE |
|
|
267
|
+
`;
|
|
268
|
+
fs.writeFileSync(localNamingPath, localNamingStub);
|
|
269
|
+
console.log(`✅ Created guidelines/objects.local.md`);
|
|
270
|
+
} else {
|
|
271
|
+
console.log(`⚠️ guidelines/objects.local.md already exists, skipped`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Detect and offer to remove old numbered guideline files (legacy cleanup)
|
|
275
|
+
await cleanupOldGuidelineFiles(guidelinesDestPath);
|
|
229
276
|
|
|
230
277
|
console.log(`
|
|
231
278
|
📋 Update complete!
|
|
232
|
-
|
|
279
|
+
Standard guidelines are read from the package automatically — no local copies needed.
|
|
280
|
+
Run 'abapgit-agent guide --migrate' if you still have copied guideline files to remove.
|
|
233
281
|
`);
|
|
234
282
|
return;
|
|
235
283
|
}
|
|
@@ -386,66 +434,55 @@ Examples:
|
|
|
386
434
|
console.log(`✅ .gitignore already up to date`);
|
|
387
435
|
}
|
|
388
436
|
|
|
389
|
-
// Copy CLAUDE.md
|
|
390
|
-
const claudeMdPath = pathModule.join(__dirname, '..', '..', 'abap', 'CLAUDE.md');
|
|
437
|
+
// Copy CLAUDE.md (slim stub — tells Claude to run 'abapgit-agent guide')
|
|
438
|
+
const claudeMdPath = pathModule.join(__dirname, '..', '..', 'abap', 'CLAUDE.slim.md');
|
|
391
439
|
const localClaudeMdPath = pathModule.join(process.cwd(), 'CLAUDE.md');
|
|
392
440
|
try {
|
|
393
|
-
if (fs.existsSync(
|
|
441
|
+
if (fs.existsSync(localClaudeMdPath)) {
|
|
442
|
+
console.log(`⚠️ CLAUDE.md already exists, skipped`);
|
|
443
|
+
} else if (fs.existsSync(claudeMdPath)) {
|
|
394
444
|
fs.copyFileSync(claudeMdPath, localClaudeMdPath);
|
|
395
445
|
console.log(`✅ Created CLAUDE.md`);
|
|
396
446
|
} else {
|
|
397
|
-
console.log(`⚠️ CLAUDE.md not found in abap/ directory`);
|
|
447
|
+
console.log(`⚠️ CLAUDE.slim.md not found in abap/ directory`);
|
|
398
448
|
}
|
|
399
449
|
} catch (error) {
|
|
400
450
|
console.error(`Error copying CLAUDE.md: ${error.message}`);
|
|
401
451
|
}
|
|
402
452
|
|
|
403
|
-
// Copy copilot-instructions.md for GitHub Copilot
|
|
404
|
-
const copilotMdPath = pathModule.join(__dirname, '..', '..', 'abap', '.github', 'copilot-instructions.md');
|
|
453
|
+
// Copy copilot-instructions.md for GitHub Copilot (slim stub)
|
|
454
|
+
const copilotMdPath = pathModule.join(__dirname, '..', '..', 'abap', '.github', 'copilot-instructions.slim.md');
|
|
405
455
|
const githubDir = pathModule.join(process.cwd(), '.github');
|
|
406
456
|
const localCopilotMdPath = pathModule.join(githubDir, 'copilot-instructions.md');
|
|
407
457
|
try {
|
|
408
|
-
if (fs.existsSync(
|
|
409
|
-
|
|
458
|
+
if (fs.existsSync(localCopilotMdPath)) {
|
|
459
|
+
console.log(`⚠️ .github/copilot-instructions.md already exists, skipped`);
|
|
460
|
+
} else if (fs.existsSync(copilotMdPath)) {
|
|
410
461
|
if (!fs.existsSync(githubDir)) {
|
|
411
462
|
fs.mkdirSync(githubDir, { recursive: true });
|
|
412
463
|
}
|
|
413
464
|
fs.copyFileSync(copilotMdPath, localCopilotMdPath);
|
|
414
465
|
console.log(`✅ Created .github/copilot-instructions.md`);
|
|
415
466
|
} else {
|
|
416
|
-
console.log(`⚠️ copilot-instructions.md not found in abap/ directory`);
|
|
467
|
+
console.log(`⚠️ copilot-instructions.slim.md not found in abap/.github/ directory`);
|
|
417
468
|
}
|
|
418
469
|
} catch (error) {
|
|
419
470
|
console.error(`Error copying copilot-instructions.md: ${error.message}`);
|
|
420
471
|
}
|
|
421
472
|
|
|
422
|
-
//
|
|
423
|
-
|
|
473
|
+
// Create guidelines/ directory and objects.local.md stub
|
|
474
|
+
// (Standard guidelines are bundled in the package — no need to copy them)
|
|
424
475
|
const guidelinesDestPath = pathModule.join(process.cwd(), 'guidelines');
|
|
425
476
|
try {
|
|
426
|
-
if (fs.existsSync(
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
// Copy all files from guidelines folder
|
|
431
|
-
const files = fs.readdirSync(guidelinesSrcPath);
|
|
432
|
-
for (const file of files) {
|
|
433
|
-
if (file.endsWith('.md')) {
|
|
434
|
-
fs.copyFileSync(
|
|
435
|
-
pathModule.join(guidelinesSrcPath, file),
|
|
436
|
-
pathModule.join(guidelinesDestPath, file)
|
|
437
|
-
);
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
console.log(`✅ Created guidelines/ (${files.filter(f => f.endsWith('.md')).length} files)`);
|
|
441
|
-
} else {
|
|
442
|
-
console.log(`⚠️ guidelines/ already exists, skipped`);
|
|
443
|
-
}
|
|
477
|
+
if (!fs.existsSync(guidelinesDestPath)) {
|
|
478
|
+
fs.mkdirSync(guidelinesDestPath, { recursive: true });
|
|
479
|
+
console.log(`✅ Created guidelines/`);
|
|
480
|
+
}
|
|
444
481
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
482
|
+
// Create objects.local.md stub if not already present
|
|
483
|
+
const localNamingPath = pathModule.join(guidelinesDestPath, 'objects.local.md');
|
|
484
|
+
if (!fs.existsSync(localNamingPath)) {
|
|
485
|
+
const localNamingStub = `---
|
|
449
486
|
nav_order: 8
|
|
450
487
|
---
|
|
451
488
|
|
|
@@ -473,14 +510,11 @@ Uncomment and edit the rows that differ from the defaults in \`guidelines/object
|
|
|
473
510
|
| Structure | Z | ZMY_STRUCTURE |
|
|
474
511
|
| Table Type | Z | ZMY_TABLE_TYPE |
|
|
475
512
|
`;
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
}
|
|
479
|
-
} else {
|
|
480
|
-
console.log(`⚠️ guidelines folder not found in abap/ directory`);
|
|
513
|
+
fs.writeFileSync(localNamingPath, localNamingStub);
|
|
514
|
+
console.log(`✅ Created guidelines/objects.local.md (project naming conventions)`);
|
|
481
515
|
}
|
|
482
516
|
} catch (error) {
|
|
483
|
-
console.error(`Error
|
|
517
|
+
console.error(`Error creating guidelines: ${error.message}`);
|
|
484
518
|
}
|
|
485
519
|
|
|
486
520
|
// Create folder
|
|
@@ -533,6 +567,11 @@ Uncomment and edit the rows that differ from the defaults in \`guidelines/object
|
|
|
533
567
|
1. Edit .abapGitAgent with your ABAP credentials (host, user, password)
|
|
534
568
|
2. Run 'abapgit-agent create --import' to create online repository
|
|
535
569
|
3. Run 'abapgit-agent pull' to activate objects
|
|
570
|
+
|
|
571
|
+
💡 Tips:
|
|
572
|
+
• Only guidelines/objects.local.md needs to live in your repo.
|
|
573
|
+
Standard guidelines are read from the package automatically via 'ref'.
|
|
574
|
+
• Run 'abapgit-agent guide' to read the full ABAP development guide.
|
|
536
575
|
`);
|
|
537
576
|
}
|
|
538
577
|
};
|
package/src/commands/pull.js
CHANGED
|
@@ -323,6 +323,13 @@ module.exports = {
|
|
|
323
323
|
if (success === 'X' || success === true) {
|
|
324
324
|
console.log(`✅ Pull completed successfully!`);
|
|
325
325
|
console.log(` Message: ${message || 'N/A'}`);
|
|
326
|
+
} else if (failedCount === 0 && failedObjects.length === 0 &&
|
|
327
|
+
activatedCount === 0 && logMessages.length === 0 &&
|
|
328
|
+
(!message || /activation cancelled|nothing to activate|already active/i.test(message))) {
|
|
329
|
+
// abapGit returns SUCCESS='' with "Activation cancelled" when there are
|
|
330
|
+
// no inactive objects to activate — the object is already active and consistent.
|
|
331
|
+
console.log(`✅ Pull completed — object already active, nothing to activate.`);
|
|
332
|
+
console.log(` Message: ${message || 'N/A'}`);
|
|
326
333
|
} else {
|
|
327
334
|
console.error(`❌ Pull completed with errors!`);
|
|
328
335
|
console.error(` Message: ${message || 'N/A'}`);
|
|
@@ -431,8 +438,12 @@ module.exports = {
|
|
|
431
438
|
console.log(`\n❌ Failed Objects Log (${failedCount})`);
|
|
432
439
|
}
|
|
433
440
|
|
|
434
|
-
// Throw if pull was not successful so callers (e.g. upgrade) can detect failure
|
|
435
|
-
|
|
441
|
+
// Throw if pull was not successful so callers (e.g. upgrade) can detect failure.
|
|
442
|
+
// Exception: SUCCESS='' with no failures and no log = object already active, not a real error.
|
|
443
|
+
const alreadyActive = failedCount === 0 && failedObjects.length === 0 &&
|
|
444
|
+
activatedCount === 0 && logMessages.length === 0 &&
|
|
445
|
+
(!message || /activation cancelled|nothing to activate|already active/i.test(message));
|
|
446
|
+
if (success !== 'X' && success !== true && !alreadyActive) {
|
|
436
447
|
const err = new Error(message || 'Pull completed with errors');
|
|
437
448
|
err._isPullError = true;
|
|
438
449
|
throw err;
|