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.
@@ -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** - See `../CLAUDE.md` for complete workflow guidance:
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
- **For standard workflow, see `../CLAUDE.md`**
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 (returns `if_cds_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 for base tables - do not specify `i_dependency_list`
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.12.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
+ };
@@ -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
 
@@ -202,34 +202,82 @@ Examples:
202
202
  console.log(`\n🔄 Updating abapGit Agent files`);
203
203
  console.log('');
204
204
 
205
- // Copy CLAUDE.md
206
- await copyFileIfExists(
207
- pathModule.join(__dirname, '..', '..', 'abap', 'CLAUDE.md'),
208
- pathModule.join(process.cwd(), 'CLAUDE.md'),
209
- 'CLAUDE.md'
210
- );
211
-
212
- // Copy copilot-instructions.md
213
- await copyFileIfExists(
214
- pathModule.join(__dirname, '..', '..', 'abap', '.github', 'copilot-instructions.md'),
215
- pathModule.join(process.cwd(), '.github', 'copilot-instructions.md'),
216
- '.github/copilot-instructions.md',
217
- true // create parent dir
218
- );
219
-
220
- // Copy guidelines folder to project root
221
- await copyGuidelinesFolder(
222
- pathModule.join(__dirname, '..', '..', 'abap', 'guidelines'),
223
- pathModule.join(process.cwd(), 'guidelines'),
224
- true // overwrite
225
- );
226
-
227
- // Detect and offer to remove old numbered guideline files
228
- await cleanupOldGuidelineFiles(pathModule.join(process.cwd(), 'guidelines'));
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
- Run 'abapgit-agent ref --list-topics' to see available topics.
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(claudeMdPath)) {
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(copilotMdPath)) {
409
- // Ensure .github directory exists
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
- // Copy guidelines folder to project root
423
- const guidelinesSrcPath = pathModule.join(__dirname, '..', '..', 'abap', 'guidelines');
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(guidelinesSrcPath)) {
427
- if (!fs.existsSync(guidelinesDestPath)) {
428
- // Create guidelines directory
429
- fs.mkdirSync(guidelinesDestPath, { recursive: true });
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
- // Create objects.local.md stub if not already present
446
- const localNamingPath = pathModule.join(guidelinesDestPath, 'objects.local.md');
447
- if (!fs.existsSync(localNamingPath)) {
448
- const localNamingStub = `---
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
- fs.writeFileSync(localNamingPath, localNamingStub);
477
- console.log(`✅ Created guidelines/objects.local.md (project naming conventions)`);
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 copying guidelines: ${error.message}`);
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
  };
@@ -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
- if (success !== 'X' && success !== true) {
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;