@cspeach/cli 1.1.24 → 1.1.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/agent/loop.js +20 -8
  2. package/dist/agent/tool-dispatch.js +6 -0
  3. package/dist/auth/auth-file.js +173 -12
  4. package/dist/classifier/client.js +55 -30
  5. package/dist/cli.js +11 -0
  6. package/dist/commands/config-set.js +13 -0
  7. package/dist/commands/config-show.js +5 -1
  8. package/dist/commands/cost.js +24 -22
  9. package/dist/commands/deploy-card.js +135 -0
  10. package/dist/commands/login.js +31 -13
  11. package/dist/commands/plan-chain.js +31 -0
  12. package/dist/commands/plan-continue.js +1 -0
  13. package/dist/commands/plan-resume.js +49 -2
  14. package/dist/cost/cost-log.js +76 -4
  15. package/dist/cost/credits-wire.js +9 -0
  16. package/dist/cost/credits.js +24 -6
  17. package/dist/cost/router-spend.js +24 -0
  18. package/dist/cost/session-cost.js +16 -2
  19. package/dist/models/server-config.js +21 -1
  20. package/dist/one-shot.js +12 -2
  21. package/dist/projects/detail-path.js +117 -0
  22. package/dist/projects/merge-cca.js +19 -0
  23. package/dist/projects/merge-upgrade.js +19 -1
  24. package/dist/projects/plan-lane.js +47 -0
  25. package/dist/projects/plan-run.js +8 -0
  26. package/dist/projects/plan-schema.js +2 -0
  27. package/dist/projects/promote.js +7 -1
  28. package/dist/projects/save-command.js +3 -2
  29. package/dist/register/records.js +7 -2
  30. package/dist/renderer/footer-line.js +10 -4
  31. package/dist/renderer/status-footer.js +5 -4
  32. package/dist/renderer/tool-labels.js +2 -1
  33. package/dist/repl.js +41 -8
  34. package/dist/router/byok-classifier.js +159 -0
  35. package/dist/router/classifier.js +130 -6
  36. package/dist/router/router-core.js +307 -0
  37. package/dist/router/router-prompt.js +121 -0
  38. package/dist/router/routing-failure.js +65 -3
  39. package/dist/skills/bundled-skills.js +45 -45
  40. package/dist/tools/_filesystem-shared.js +363 -33
  41. package/dist/tools/cap/metadata-import.js +10 -4
  42. package/dist/tools/filesystem/file-write.js +12 -39
  43. package/dist/tools/filesystem/glob.js +25 -2
  44. package/dist/tools/filesystem/grep.js +25 -2
  45. package/dist/tools/fiori/apply.js +30 -21
  46. package/dist/tools/fiori/commit-gate.js +58 -0
  47. package/dist/tools/fiori/deploy/existing-bsp.js +67 -0
  48. package/dist/tools/fiori/deploy/run.js +608 -0
  49. package/dist/tools/fiori/deploy/scope.js +8 -0
  50. package/dist/tools/fiori/deploy/yaml-read.js +236 -0
  51. package/dist/tools/fiori/deploy-config.js +24 -6
  52. package/dist/tools/fiori/fe-extend.js +6 -1
  53. package/dist/tools/fiori/fe-scaffold.js +6 -1
  54. package/dist/tools/fiori/path-params.js +78 -0
  55. package/dist/tools/fiori/scaffold.js +2 -1
  56. package/dist/tools/fiori/tools.js +170 -19
  57. package/dist/tools/local-build.js +3 -0
  58. package/dist/tools/project/_merge-shared.js +36 -1
  59. package/dist/tools/project/cca_merge.js +4 -0
  60. package/dist/tools/project/upgrade_merge_progress.js +21 -10
  61. package/dist/tools/subagent/read_agent.js +4 -2
  62. package/dist/tools/tool-loading.js +1 -1
  63. package/package.json +1 -1
@@ -5,7 +5,7 @@ import { getEntry } from './catalog/index.js';
5
5
  import { renderEntry } from './render.js';
6
6
  import { mergeManifest } from './manifest.js';
7
7
  import { appendI18n } from './i18n.js';
8
- import { resolveSafePath } from '../_filesystem-shared.js';
8
+ import { resolveSafePath, assertWritableSync } from '../_filesystem-shared.js';
9
9
  /**
10
10
  * Render a catalog entry and apply it into an existing scaffolded app on disk.
11
11
  *
@@ -15,35 +15,44 @@ import { resolveSafePath } from '../_filesystem-shared.js';
15
15
  * CUSTOMER params (e.g. `webapp/ext/fragment/<%- ns %>Chart.fragment.xml`), so a
16
16
  * hostile param could otherwise carry `..` and escape `appDir`. Every path this
17
17
  * function writes — rendered files, manifest.json, and the i18n file (whose
18
- * relative location is itself read from on-disk manifest data) — is resolved
19
- * through `resolveSafePath(opts.appDir, rel)`, which throws PathOutsideRootError
20
- * if the resolved absolute path is not contained under appDir. This is the
21
- * authoritative defense; the catalog-side `ns` pattern (render.ts) is the
22
- * second, earlier layer.
18
+ * relative location is itself read from on-disk manifest data) — goes through
19
+ * `resolveSafePath(opts.appDir, rel)` (string containment) and then
20
+ * `assertWritableSync` (real path after links / short names must stay inside
21
+ * appDir and must not be a protected path such as a nested `.git`). ALL
22
+ * targets are checked BEFORE the first write, so a refusal writes nothing.
23
+ * The catalog-side `ns` pattern (render.ts) is the second, earlier layer.
23
24
  */
24
25
  export function applyEntry(opts) {
25
26
  const rendered = renderEntry(getEntry(opts.entryName), opts.params);
26
- // 1. write files — re-validate EVERY rendered (param-derived) path against appDir.
27
- for (const f of rendered.files) {
28
- const abs = resolveSafePath(opts.appDir, f.path);
29
- mkdirSync(dirname(abs), { recursive: true });
30
- writeFileSync(abs, f.content, 'utf8');
31
- }
32
- // 2. merge manifest — ALWAYS re-read from disk (I2). Fixed relative path, but
33
- // routed through resolveSafePath for uniform containment.
34
- const manifestPath = resolveSafePath(opts.appDir, 'webapp/manifest.json');
27
+ const denyRoot = opts.denyRoot ?? opts.appDir;
28
+ const target = (rel) => {
29
+ const abs = resolveSafePath(opts.appDir, rel);
30
+ assertWritableSync(abs, opts.appDir, denyRoot);
31
+ return abs;
32
+ };
33
+ // 0. Resolve + verify every write target first.
34
+ const files = rendered.files.map((f) => ({ abs: target(f.path), content: f.content }));
35
+ // manifest — ALWAYS re-read from disk (I2). Fixed relative path, still contained.
36
+ const manifestPath = target('webapp/manifest.json');
35
37
  const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
36
38
  let merged = mergeManifest(manifest, rendered.manifestPatch);
37
39
  for (const lib of rendered.libraries) {
38
40
  merged = mergeManifest(merged, { 'sap.ui5': { dependencies: { libs: { [lib]: {} } } } });
39
41
  }
40
- writeFileSync(manifestPath, JSON.stringify(merged, null, 2), 'utf8');
41
- // 3. append i18n — resolve path from sap.app.i18n (M1). i18nRel is on-disk
42
- // data, not a render param, but it still drives a write target, so contain it.
42
+ // i18n — path from sap.app.i18n (M1): on-disk data that drives a write target.
43
43
  const i18nRel = merged['sap.app']?.i18n ?? 'i18n/i18n.properties';
44
- const i18nPath = resolveSafePath(opts.appDir, `webapp/${i18nRel}`);
45
- const existing = existsSync(i18nPath) ? readFileSync(i18nPath, 'utf8') : '';
46
- if (Object.keys(rendered.i18nKeys).length) {
44
+ const hasI18n = Object.keys(rendered.i18nKeys).length > 0;
45
+ const i18nPath = hasI18n ? target(`webapp/${i18nRel}`) : null;
46
+ // 1. write files.
47
+ for (const f of files) {
48
+ mkdirSync(dirname(f.abs), { recursive: true });
49
+ writeFileSync(f.abs, f.content, 'utf8');
50
+ }
51
+ // 2. merge manifest.
52
+ writeFileSync(manifestPath, JSON.stringify(merged, null, 2), 'utf8');
53
+ // 3. append i18n.
54
+ if (i18nPath) {
55
+ const existing = existsSync(i18nPath) ? readFileSync(i18nPath, 'utf8') : '';
47
56
  mkdirSync(dirname(i18nPath), { recursive: true });
48
57
  writeFileSync(i18nPath, appendI18n(existing, rendered.i18nKeys), 'utf8');
49
58
  }
@@ -0,0 +1,58 @@
1
+ // cspeach-cli/src/tools/fiori/commit-gate.ts
2
+ /**
3
+ * Commit-time containment gate for every mem-fs editor the Fiori engine
4
+ * flushes (scaffoldFreestyle, scaffoldFioriElements, feExtend).
5
+ *
6
+ * The @sap-ux writers build paths from caller-supplied config (`folder`,
7
+ * `name`, `localAnnotationsName`, event-handler file names, …). Validating
8
+ * each known param is the first layer; THIS is the authoritative one: before
9
+ * anything reaches disk, every pending path in the editor's store must land —
10
+ * after following links and expanding Windows short names — inside the app
11
+ * folder, and must not be a protected path (`.git` at any depth, `.cspeach`,
12
+ * `.env*`, the CLI home). One bad path refuses the WHOLE commit, so nothing
13
+ * is half-written.
14
+ */
15
+ import * as path from 'node:path';
16
+ import { assertWritable, PathOutsideRootError, ProtectedPathError } from '../_filesystem-shared.js';
17
+ /** Thrown when a pending write would leave the app folder or hit a protected path. */
18
+ export class UnsafeCommitError extends Error {
19
+ constructor(detail) {
20
+ super(`refusing to write the app: ${detail}. Nothing was written.`);
21
+ this.name = 'UnsafeCommitError';
22
+ }
23
+ }
24
+ /** Every path the editor would write or delete on commit. */
25
+ function pendingPaths(editor) {
26
+ const store = editor.store;
27
+ if (!store || typeof store.each !== 'function') {
28
+ // Without a store we cannot see what commit would touch — fail closed.
29
+ throw new UnsafeCommitError('the pending file list could not be read');
30
+ }
31
+ const out = [];
32
+ store.each((f) => {
33
+ if (f && (f.state === 'modified' || f.state === 'deleted'))
34
+ out.push(f.path);
35
+ });
36
+ return out;
37
+ }
38
+ /**
39
+ * Verify every pending path, then commit. `appDir` is the containment root;
40
+ * `denyRoot` (default appDir) is the project root the denylist is judged
41
+ * against — tools pass ctx.cwd so a protected folder ABOVE the app is caught.
42
+ */
43
+ export async function commitContained(editor, appDir, denyRoot = appDir) {
44
+ for (const p of pendingPaths(editor)) {
45
+ try {
46
+ await assertWritable(path.resolve(p), appDir, denyRoot);
47
+ }
48
+ catch (err) {
49
+ const shown = path.relative(path.resolve(denyRoot), path.resolve(p)).replace(/\\/g, '/');
50
+ if (err instanceof PathOutsideRootError)
51
+ throw new UnsafeCommitError(`"${shown}" is outside the app folder`);
52
+ if (err instanceof ProtectedPathError)
53
+ throw new UnsafeCommitError(`"${shown}" is a protected path`);
54
+ throw err;
55
+ }
56
+ }
57
+ await new Promise((res, rej) => editor.commit((e) => (e ? rej(e) : res())));
58
+ }
@@ -0,0 +1,67 @@
1
+ // cspeach-cli/src/tools/fiori/deploy/existing-bsp.ts
2
+ /**
3
+ * fiori_deploy pre-check — does the target BSP already exist, and whose app is it?
4
+ *
5
+ * `fiori deploy --yes` would silently overwrite a BSP that holds ANOTHER app
6
+ * (the tooling's HTTP 412 "different sap app id" handler auto-confirms with
7
+ * --yes), which could be another team's application. So before either mode
8
+ * runs, two read-only GETs over the alias's authenticated connection:
9
+ *
10
+ * 1. /sap/opu/odata/UI5/ABAP_REPOSITORY_SRV/Repositories('<BSP>') — the same
11
+ * OData service the deploy itself uploads through. 404 → a new app;
12
+ * 200 → it exists; anything else (401/403/500, no answer) → the check
13
+ * could not run, and the deploy is refused rather than guessed.
14
+ * 2. when it exists: /sap/bc/ui5_ui5/sap/<bsp>/manifest.json → sap.app.id.
15
+ * If that cannot be read, the app id is UNKNOWN, which is treated like a
16
+ * different app (never assumed to be ours).
17
+ */
18
+ function repositoryPath(bsp) {
19
+ return `/sap/opu/odata/UI5/ABAP_REPOSITORY_SRV/Repositories('${encodeURIComponent(bsp)}')?$format=json`;
20
+ }
21
+ /** /sap/bc/ui5_ui5/sap/<bsp>/<file>, or /sap/bc/ui5_ui5/<ns>/<name>/<file> for a namespaced BSP. */
22
+ export function bspFilePath(bsp, file) {
23
+ const lower = bsp.toLowerCase();
24
+ const ns = /^\/([a-z0-9_]+)\/([a-z0-9_]+)$/.exec(lower);
25
+ return ns ? `/sap/bc/ui5_ui5/${ns[1]}/${ns[2]}/${file}` : `/sap/bc/ui5_ui5/sap/${lower}/${file}`;
26
+ }
27
+ export async function readExistingBsp(bsp, get) {
28
+ let repo;
29
+ try {
30
+ repo = await get(repositoryPath(bsp));
31
+ }
32
+ catch (e) {
33
+ return { kind: 'unknown', reason: `the ABAP repository service did not answer (${e instanceof Error ? e.message : String(e)})` };
34
+ }
35
+ if (repo.status === 404)
36
+ return { kind: 'new' };
37
+ if (repo.status !== 200) {
38
+ return {
39
+ kind: 'unknown',
40
+ reason: `the ABAP repository service answered HTTP ${repo.status}` +
41
+ (repo.status === 401 || repo.status === 403 ? ' (the SAP user may lack authorisation for /UI5/ABAP_REPOSITORY_SRV)' : ''),
42
+ };
43
+ }
44
+ let description;
45
+ let pkg;
46
+ try {
47
+ const d = JSON.parse(repo.body ?? '').d;
48
+ description = d?.Description || undefined;
49
+ pkg = d?.Package || undefined;
50
+ }
51
+ catch { /* the entry exists; its details are only for the message */ }
52
+ let appId = null;
53
+ try {
54
+ const m = await get(bspFilePath(bsp, 'manifest.json'));
55
+ if (m.status === 200) {
56
+ const id = JSON.parse(m.body ?? '')['sap.app']?.id;
57
+ if (typeof id === 'string' && id.trim() !== '')
58
+ appId = id.trim();
59
+ }
60
+ }
61
+ catch { /* unknown app id — treated as a different app */ }
62
+ return { kind: 'existing', appId, ...(description ? { description } : {}), ...(pkg ? { package: pkg } : {}) };
63
+ }
64
+ /** The approval object an overwrite must carry: `<BSP> overwrites app id <id|unknown>`. */
65
+ export function overwriteApprovalObject(bsp, remoteAppId) {
66
+ return `${bsp} overwrites app id ${remoteAppId ?? 'unknown'}`;
67
+ }