@occam-scaly/scaly-cli 0.1.0 → 0.2.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 CHANGED
@@ -1,6 +1,9 @@
1
1
  # @occam-scaly/scaly-cli
2
2
 
3
- Scaly CLI for end users. Primary use today is enabling advanced Scaly MCP tools (databases, storage, logs) without copying tokens into editor config or restarting your IDE.
3
+ Scaly CLI for end users. Enables:
4
+
5
+ - `scaly auth login` (session token for advanced MCP tools: databases/storage/logs)
6
+ - `.scaly/config.yaml` workflows (`scaly plan` / `scaly apply` / `scaly pull`)
4
7
 
5
8
  ## Install (recommended)
6
9
 
@@ -22,6 +25,16 @@ scaly auth status
22
25
  scaly auth logout
23
26
  ```
24
27
 
28
+ ## Project config
29
+
30
+ From a project repo containing `.scaly/config.yaml`:
31
+
32
+ ```bash
33
+ scaly plan --json
34
+ scaly apply --auto-approve --plan-hash sha256:... --json
35
+ scaly pull --json
36
+ ```
37
+
25
38
  ### Stages
26
39
 
27
40
  ```bash
package/lib/scaly-api.js CHANGED
@@ -1,23 +1,103 @@
1
1
  'use strict';
2
2
 
3
3
  const axios = require('axios');
4
+ const fs = require('fs');
5
+ const os = require('os');
6
+ const path = require('path');
4
7
 
5
8
  const STAGE_ENDPOINTS = {
6
- prod: 'https://api.scaly.cloud/graphql',
7
- dev: 'https://api-dev.scaly.cloud/graphql',
8
- staging: 'https://api-staging.scaly.cloud/graphql'
9
+ prod: 'https://api.scalyapps.io/graphql',
10
+ dev: 'https://api.dev.scalyapps.io/graphql',
11
+ qa: 'https://api.qa.scalyapps.io/graphql'
9
12
  };
10
13
 
11
14
  function resolveApiEndpoint() {
12
15
  return (
13
16
  process.env.SCALY_API_URL ||
14
17
  process.env.API_ENDPOINT ||
15
- STAGE_ENDPOINTS[process.env.SCALY_STAGE || 'prod'] ||
18
+ STAGE_ENDPOINTS[normalizeStage(process.env.SCALY_STAGE) || 'prod'] ||
16
19
  STAGE_ENDPOINTS.prod
17
20
  );
18
21
  }
19
22
 
23
+ function normalizeStage(stage) {
24
+ const s = String(stage || '')
25
+ .trim()
26
+ .toLowerCase();
27
+ if (!s) return 'prod';
28
+ if (s === 'production') return 'prod';
29
+ if (s === 'staging') return 'qa';
30
+ return s;
31
+ }
32
+
33
+ function decodeBase64Url(input) {
34
+ const padLen = (4 - (input.length % 4)) % 4;
35
+ const padded =
36
+ input.replace(/-/g, '+').replace(/_/g, '/') + '='.repeat(padLen);
37
+ return Buffer.from(padded, 'base64').toString('utf8');
38
+ }
39
+
40
+ function tryGetJwtExpSeconds(token) {
41
+ if (!token || typeof token !== 'string') return null;
42
+ const parts = token.split('.');
43
+ if (parts.length !== 3) return null;
44
+ try {
45
+ const payloadJson = decodeBase64Url(parts[1] || '');
46
+ const payload = JSON.parse(payloadJson);
47
+ const exp = payload && payload.exp;
48
+ return typeof exp === 'number' ? exp : null;
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
53
+
54
+ function isJwtExpired(token, nowMs = Date.now()) {
55
+ const expSeconds = tryGetJwtExpSeconds(token);
56
+ if (!expSeconds) return false;
57
+ return nowMs >= expSeconds * 1000;
58
+ }
59
+
60
+ function resolveAuthStorePath() {
61
+ if (process.env.SCALY_AUTH_STORE_PATH)
62
+ return String(process.env.SCALY_AUTH_STORE_PATH);
63
+ const base =
64
+ process.env.SCALY_CONFIG_DIR ||
65
+ process.env.XDG_CONFIG_HOME ||
66
+ path.join(os.homedir(), '.config');
67
+ const primary = path.join(base, 'scaly', 'auth.json');
68
+ if (fs.existsSync(primary)) return primary;
69
+ const legacy = path.join(os.homedir(), '.scaly', 'auth.json');
70
+ if (fs.existsSync(legacy)) return legacy;
71
+ return primary;
72
+ }
73
+
74
+ function tryReadAuthStoreToken() {
75
+ const p = resolveAuthStorePath();
76
+ try {
77
+ const text = fs.readFileSync(p, 'utf8');
78
+ const obj = JSON.parse(text);
79
+ const token =
80
+ obj && typeof obj.access_token === 'string' ? obj.access_token : null;
81
+ if (!token) return null;
82
+ const expiresAtMs =
83
+ typeof obj.expires_at === 'number' ? obj.expires_at : null;
84
+ if (expiresAtMs && Date.now() >= expiresAtMs) return null;
85
+ if (isJwtExpired(token)) return null;
86
+ return token;
87
+ } catch {
88
+ return null;
89
+ }
90
+ }
91
+
20
92
  function resolveAuth() {
93
+ const scalyApiKey = process.env.SCALY_API_KEY;
94
+
95
+ const oidcToken =
96
+ process.env.SCALY_OIDC_TOKEN || tryReadAuthStoreToken() || null;
97
+ if (oidcToken && scalyApiKey) {
98
+ return { mode: 'oidc', token: oidcToken, scalyApiKey };
99
+ }
100
+
21
101
  const bearer =
22
102
  process.env.SCALY_API_BEARER ||
23
103
  process.env.API_BEARER_TOKEN ||
@@ -26,8 +106,16 @@ function resolveAuth() {
26
106
  return { mode: 'bearer', token: bearer };
27
107
  }
28
108
 
29
- const scalyApiKey = process.env.SCALY_API_KEY;
30
- const appSyncApiKey = process.env.SCALY_APPSYNC_KEY;
109
+ const stage = normalizeStage(process.env.SCALY_STAGE);
110
+ const appSyncApiKey =
111
+ process.env.SCALY_APPSYNC_KEY ||
112
+ {
113
+ prod: 'da2-2veyt7vqrbaqbjio56tcgpbboe',
114
+ dev: 'da2-ujszvu6uwbenxfbtuy6svpffxi',
115
+ qa: 'da2-riqkqxfjpjfihewbm4e2buq2xm'
116
+ }[stage] ||
117
+ null;
118
+
31
119
  if (scalyApiKey && appSyncApiKey) {
32
120
  return { mode: 'keys', scalyApiKey, appSyncApiKey };
33
121
  }
@@ -40,6 +128,12 @@ function buildHeaders(auth) {
40
128
  if (auth.mode === 'bearer') {
41
129
  return { authorization: `Bearer ${auth.token}` };
42
130
  }
131
+ if (auth.mode === 'oidc') {
132
+ return {
133
+ authorization: `Bearer ${auth.token}`,
134
+ 'x-scaly-api-key': auth.scalyApiKey
135
+ };
136
+ }
43
137
  return {
44
138
  'x-api-key': auth.appSyncApiKey,
45
139
  'x-scaly-api-key': auth.scalyApiKey
@@ -51,7 +145,7 @@ async function graphqlRequest(query, variables) {
51
145
  const auth = resolveAuth();
52
146
  if (!auth) {
53
147
  const e = new Error(
54
- 'Missing auth. Provide SCALY_API_BEARER (or run `scaly login`) OR set SCALY_API_KEY + SCALY_APPSYNC_KEY.'
148
+ 'Missing auth. Set SCALY_API_KEY. For advanced planning (add-ons), run `scaly auth login` to create a session token.'
55
149
  );
56
150
  e.code = 'SCALY_AUTH_MISSING';
57
151
  throw e;
package/package.json CHANGED
@@ -1,15 +1,17 @@
1
1
  {
2
2
  "name": "@occam-scaly/scaly-cli",
3
- "version": "0.1.0",
4
- "description": "Scaly CLI (auth + MCP helpers)",
3
+ "version": "0.2.1",
4
+ "description": "Scaly CLI (auth + project config helpers)",
5
5
  "bin": {
6
- "scaly": "./bin/scaly-public.js"
6
+ "scaly": "./bin/scaly.js"
7
7
  },
8
8
  "scripts": {
9
- "test": "node -c ./bin/scaly-public.js && node -c ./lib/scaly-auth.js"
9
+ "test": "node -c ./bin/scaly.js && node -c ./lib/scaly-auth.js"
10
10
  },
11
11
  "dependencies": {
12
- "axios": "^1.7.9"
12
+ "axios": "^1.7.9",
13
+ "ws": "^8.18.3",
14
+ "yaml": "^2.8.1"
13
15
  },
14
16
  "engines": {
15
17
  "node": ">=18.0.0"
@@ -24,4 +26,3 @@
24
26
  "README.md"
25
27
  ]
26
28
  }
27
-