@jacebenson/jsn 4.3.5 → 4.3.6-dev.10
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/LICENSE +21 -0
- package/README.md +1 -1
- package/package.json +2 -5
- package/skills/servicenow/SKILL.md +9 -11
- package/src/commands/catalog/enrich.js +1 -3
- package/src/commands/updatesets.js +149 -9
- package/src/config.js +8 -2
- package/scripts/pre-commit-check.sh +0 -61
- package/scripts/release.js +0 -62
- package/scripts/tag-reminder.js +0 -30
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jace Benson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -193,7 +193,7 @@ npm install
|
|
|
193
193
|
npm test
|
|
194
194
|
```
|
|
195
195
|
|
|
196
|
-
Releases
|
|
196
|
+
Releases run from the GitHub Actions workflow. Dispatch `Release production` with `patch`, `minor`, or `major`. The workflow tests and lints the repo, bumps the version, pushes the commit and tag, dispatches the npm publish workflow, and creates the GitHub release.
|
|
197
197
|
|
|
198
198
|
## License
|
|
199
199
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jacebenson/jsn",
|
|
3
|
-
"version": "4.3.
|
|
3
|
+
"version": "4.3.6-dev.10",
|
|
4
4
|
"description": "A command-line interface for ServiceNow that follows the Unix philosophy: simple, composable, and scriptable.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,9 +12,7 @@
|
|
|
12
12
|
"lint": "npx eslint src/ bin/ test/",
|
|
13
13
|
"start": "node bin/jsn.js",
|
|
14
14
|
"docs": "npm --prefix docs run build",
|
|
15
|
-
"docs:serve": "npm --prefix docs run serve"
|
|
16
|
-
"tag-reminder": "node scripts/tag-reminder.js",
|
|
17
|
-
"release": "node scripts/release.js"
|
|
15
|
+
"docs:serve": "npm --prefix docs run serve"
|
|
18
16
|
},
|
|
19
17
|
"keywords": [
|
|
20
18
|
"servicenow",
|
|
@@ -43,7 +41,6 @@
|
|
|
43
41
|
"bin/jsn.js",
|
|
44
42
|
"src/",
|
|
45
43
|
"skills/",
|
|
46
|
-
"scripts/",
|
|
47
44
|
"README.md",
|
|
48
45
|
"LICENSE"
|
|
49
46
|
]
|
|
@@ -14,7 +14,7 @@ compatibility: |
|
|
|
14
14
|
Works with Claude Code, OpenCode, Cursor, and agentskills-compatible tools.
|
|
15
15
|
metadata:
|
|
16
16
|
author: jacebenson
|
|
17
|
-
version: "4.
|
|
17
|
+
version: "4.3.6"
|
|
18
18
|
repository: https://github.com/jacebenson/jsn
|
|
19
19
|
---
|
|
20
20
|
|
|
@@ -65,7 +65,7 @@ Pick the most specific tool for the job. **Never default to eval** — it's the
|
|
|
65
65
|
|--------|---------|
|
|
66
66
|
| **Core** | `incidents`, `changes`, `requests`, `tasks` |
|
|
67
67
|
| **Catalog** | `catalogitems list/show/create` — items with variables and flow info |
|
|
68
|
-
| **Automation** | `flows`, `actions`, `rules`, `
|
|
68
|
+
| **Automation** | `flows`, `actions`, `rules`, `triggers`, `scheduledjobs` |
|
|
69
69
|
| **Access** | `acls`, `b4rules`, `roles`, `groups`, `users` |
|
|
70
70
|
| **UX** | `forms` (section/element layout), `lists`, `clientscripts`, `uipolicies` |
|
|
71
71
|
| **Data** | `tables`, `columns`, `includes`, `logs`, `properties`, `records` |
|
|
@@ -73,16 +73,16 @@ Pick the most specific tool for the job. **Never default to eval** — it's the
|
|
|
73
73
|
| **Docs** | `docs sync`, `docs status`, `docs search`, `docs serve` |
|
|
74
74
|
| **Developer** | `eval`, `rest`, `skill`, `version` |
|
|
75
75
|
|
|
76
|
-
###
|
|
76
|
+
### Table and raw REST examples
|
|
77
77
|
|
|
78
78
|
```bash
|
|
79
|
-
# Query
|
|
80
|
-
jsn
|
|
79
|
+
# Query a table with the normal records command
|
|
80
|
+
jsn records list --table incident --query "active=true" --limit 5 --json
|
|
81
81
|
|
|
82
|
-
#
|
|
83
|
-
jsn
|
|
82
|
+
# Get a single record
|
|
83
|
+
jsn records get --table incident --sys-id abc123... --json
|
|
84
84
|
|
|
85
|
-
# Raw endpoint access
|
|
85
|
+
# Raw endpoint access when no specific command fits
|
|
86
86
|
jsn rest "/api/now/table/incident?sysparm_limit=3" --json
|
|
87
87
|
```
|
|
88
88
|
|
|
@@ -106,8 +106,6 @@ jsn docs search "REST API" --bundle it-service-management --json
|
|
|
106
106
|
# Start the web UI
|
|
107
107
|
jsn docs serve
|
|
108
108
|
|
|
109
|
-
# Share on your network
|
|
110
|
-
jsn docs serve --expose
|
|
111
109
|
```
|
|
112
110
|
|
|
113
111
|
**Re-running `jsn docs sync`** does a smart incremental refresh — pulls latest docs and only rebuilds changed files (seconds, not minutes).
|
|
@@ -129,5 +127,5 @@ Check order (priority):
|
|
|
129
127
|
```
|
|
130
128
|
~/.config/servicenow/
|
|
131
129
|
├── config.json # Profiles and settings
|
|
132
|
-
└── credentials
|
|
130
|
+
└── credentials/ # Auth tokens (fallback, one file per key)
|
|
133
131
|
```
|
|
@@ -68,7 +68,6 @@ export async function enrichCatalogItem({ sdk, instanceUrl, sysID }) {
|
|
|
68
68
|
flowCmd = `jsn flows show ${flowVal.value}`;
|
|
69
69
|
}
|
|
70
70
|
const wfName = getStringField(item, 'workflow') || '';
|
|
71
|
-
const wfID = item.workflow?.value || '';
|
|
72
71
|
const planName = getStringField(item, 'delivery_plan') || getStringField(item, 'execution_plan') || '';
|
|
73
72
|
|
|
74
73
|
const data = {
|
|
@@ -80,7 +79,7 @@ export async function enrichCatalogItem({ sdk, instanceUrl, sysID }) {
|
|
|
80
79
|
flow: flowName || undefined,
|
|
81
80
|
flow_cmd: flowCmd || undefined,
|
|
82
81
|
workflow: wfName || undefined,
|
|
83
|
-
|
|
82
|
+
|
|
84
83
|
execution_plan: planName || undefined,
|
|
85
84
|
variables: totalVars > 0 ? {
|
|
86
85
|
count: totalVars,
|
|
@@ -117,7 +116,6 @@ export function buildCatalogFormatted(data, standalone, setVars, totalVars) {
|
|
|
117
116
|
if (data.workflow) {
|
|
118
117
|
lines.push('─ Workflow ─');
|
|
119
118
|
lines.push(` ${data.workflow}`);
|
|
120
|
-
if (data.workflow_cmd) lines.push(` → ${data.workflow_cmd}`);
|
|
121
119
|
lines.push('');
|
|
122
120
|
}
|
|
123
121
|
if (totalVars > 0) {
|
|
@@ -88,14 +88,144 @@ export async function resolveUpdateSetByName(app, name) {
|
|
|
88
88
|
throw errUsage(`Multiple update sets named "${name}" (${records.length}): ${scopes}.${scopeNote}\nRun bare "jsn updatesets set" and pick by scope.`);
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
|
|
92
|
+
function rawReferenceId(value) {
|
|
93
|
+
if (!value) return '';
|
|
94
|
+
if (typeof value === 'object') return String(value.value || value.sys_id || '');
|
|
95
|
+
return String(value);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function referenceName(value) {
|
|
99
|
+
if (!value) return '';
|
|
100
|
+
if (typeof value === 'object') return String(value.display_value || value.name || value.value || value.sys_id || '');
|
|
101
|
+
return String(value);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function updateSetApplication(record) {
|
|
105
|
+
const appName = getStringField(record, 'application') || '';
|
|
106
|
+
const scopeName = getStringField(record, 'application.scope') || '';
|
|
107
|
+
return appName && scopeName ? `${appName}/${scopeName}` : (appName || scopeName || '?');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function updateSetHierarchyRow(record, { depth, kind, selected = false, missing = false } = {}) {
|
|
111
|
+
return {
|
|
112
|
+
sys_id: getStringField(record, 'sys_id'),
|
|
113
|
+
name: getStringField(record, 'name') || referenceName(record),
|
|
114
|
+
state: getStringField(record, 'state') || '?',
|
|
115
|
+
application: updateSetApplication(record),
|
|
116
|
+
scope: updateSetApplication(record),
|
|
117
|
+
parent: referenceName(record.parent),
|
|
118
|
+
depth,
|
|
119
|
+
kind,
|
|
120
|
+
selected,
|
|
121
|
+
missing,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function fetchUpdateSetById(app, sysID) {
|
|
126
|
+
if (!sysID) return null;
|
|
127
|
+
const params = new URLSearchParams();
|
|
128
|
+
params.set('sysparm_query', `sys_id=${sysID}`);
|
|
129
|
+
params.set('sysparm_limit', '1');
|
|
130
|
+
params.set('sysparm_display_value', 'all');
|
|
131
|
+
params.set('sysparm_fields', 'sys_id,name,state,application,application.scope,parent,description,sys_created_on,sys_updated_on');
|
|
132
|
+
const records = await app.sdk.list('sys_update_set', params);
|
|
133
|
+
return Array.isArray(records) && records.length > 0 ? records[0] : null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Build a safe, flat preorder hierarchy with ancestors before the selected set. */
|
|
137
|
+
export async function buildUpdateSetHierarchy(app, selectedRecord) {
|
|
138
|
+
const selectedID = getStringField(selectedRecord, 'sys_id');
|
|
139
|
+
const visited = new Set(selectedID ? [selectedID] : []);
|
|
140
|
+
const cycleIds = new Set();
|
|
141
|
+
const missingParents = [];
|
|
142
|
+
const ancestors = [];
|
|
143
|
+
let parentRef = selectedRecord?.parent;
|
|
144
|
+
|
|
145
|
+
while (rawReferenceId(parentRef)) {
|
|
146
|
+
const parentID = rawReferenceId(parentRef);
|
|
147
|
+
const parentDisplay = referenceName(parentRef);
|
|
148
|
+
if (visited.has(parentID)) {
|
|
149
|
+
cycleIds.add(parentID);
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
visited.add(parentID);
|
|
153
|
+
let parent;
|
|
154
|
+
try { parent = await fetchUpdateSetById(app, parentID); } catch { parent = null; }
|
|
155
|
+
if (!parent) {
|
|
156
|
+
missingParents.push(parentID);
|
|
157
|
+
ancestors.push({ sys_id: parentID, name: parentDisplay || `[missing parent ${parentID}]`, parent: null, missing: true });
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
ancestors.push(parent);
|
|
161
|
+
parentRef = parent.parent;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const descendants = [];
|
|
165
|
+
async function walkChildren(parentID, depth) {
|
|
166
|
+
if (!parentID) return;
|
|
167
|
+
let children;
|
|
168
|
+
try {
|
|
169
|
+
const params = new URLSearchParams();
|
|
170
|
+
params.set('sysparm_query', `parent=${parentID}^ORDERBYname`);
|
|
171
|
+
params.set('sysparm_limit', '500');
|
|
172
|
+
params.set('sysparm_display_value', 'all');
|
|
173
|
+
params.set('sysparm_fields', 'sys_id,name,state,application,application.scope,parent,description,sys_created_on,sys_updated_on');
|
|
174
|
+
children = await app.sdk.list('sys_update_set', params);
|
|
175
|
+
} catch {
|
|
176
|
+
children = [];
|
|
177
|
+
}
|
|
178
|
+
if (!Array.isArray(children)) return;
|
|
179
|
+
for (const child of children) {
|
|
180
|
+
const childID = getStringField(child, 'sys_id');
|
|
181
|
+
if (!childID) continue;
|
|
182
|
+
if (visited.has(childID)) {
|
|
183
|
+
cycleIds.add(childID);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
visited.add(childID);
|
|
187
|
+
descendants.push({ record: child, depth });
|
|
188
|
+
await walkChildren(childID, depth + 1);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const hierarchy = [
|
|
193
|
+
...ancestors.reverse().map((record, index) => updateSetHierarchyRow(record, { depth: index, kind: 'ancestor', missing: record.missing })),
|
|
194
|
+
updateSetHierarchyRow(selectedRecord, { depth: ancestors.length, kind: 'selected', selected: true }),
|
|
195
|
+
];
|
|
196
|
+
await walkChildren(selectedID, ancestors.length + 1);
|
|
197
|
+
hierarchy.push(...descendants.map(({ record, depth }) => updateSetHierarchyRow(record, { depth, kind: 'descendant' })));
|
|
198
|
+
|
|
199
|
+
return { hierarchy, cycleIds: [...cycleIds], missingParents };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function renderUpdateSetHierarchy(rows, cycleIds = [], missingParents = []) {
|
|
203
|
+
const lines = ['Update set hierarchy:'];
|
|
204
|
+
const hasLaterSibling = (index, depth) => {
|
|
205
|
+
for (let i = index + 1; i < rows.length; i += 1) {
|
|
206
|
+
if (rows[i].depth < depth) return false;
|
|
207
|
+
if (rows[i].depth === depth) return true;
|
|
208
|
+
}
|
|
209
|
+
return false;
|
|
210
|
+
};
|
|
211
|
+
rows.forEach((row, index) => {
|
|
212
|
+
const continuation = [];
|
|
213
|
+
for (let level = 0; level < row.depth - 1; level += 1) {
|
|
214
|
+
continuation.push(hasLaterSibling(index, level) ? '│ ' : ' ');
|
|
215
|
+
}
|
|
216
|
+
const connector = row.depth > 0 ? (hasLaterSibling(index, row.depth) ? '├── ' : '└── ') : '';
|
|
217
|
+
const marker = row.selected ? '◉' : '○';
|
|
218
|
+
const missing = row.missing ? ' [missing parent]' : '';
|
|
219
|
+
lines.push(`${continuation.join('')}${connector}${marker} ${row.name}${missing} [${row.state}] (${row.scope})`);
|
|
220
|
+
});
|
|
221
|
+
if (missingParents.length > 0) lines.push(` ⚠️ Missing parent records: ${missingParents.join(', ')}`);
|
|
222
|
+
if (cycleIds.length > 0) lines.push(` ⚠️ Hierarchy cycle detected; repeated branch omitted (${cycleIds.join(', ')})`);
|
|
223
|
+
return lines;
|
|
224
|
+
}
|
|
225
|
+
|
|
91
226
|
/**
|
|
92
|
-
* Rich display for an update set: header fields
|
|
93
|
-
*
|
|
94
|
-
* descending (newest first). Shared by `list` pick and `show`.
|
|
95
|
-
*
|
|
96
|
-
* Fetches an enriched record itself (sysparm_display_value=all so the parent
|
|
97
|
-
* reference renders as a name, not a sys_id) — callers can pass a thin
|
|
98
|
-
* picker record; the formatter completes it.
|
|
227
|
+
* Rich display for an update set: header fields, update-set hierarchy, and
|
|
228
|
+
* separate child update payload details.
|
|
99
229
|
*/
|
|
100
230
|
export async function formatUpdateSetDetail(app, record) {
|
|
101
231
|
const sysID = getStringField(record, 'sys_id');
|
|
@@ -109,7 +239,7 @@ export async function formatUpdateSetDetail(app, record) {
|
|
|
109
239
|
params.set('sysparm_query', `sys_id=${sysID}`);
|
|
110
240
|
params.set('sysparm_limit', '1');
|
|
111
241
|
params.set('sysparm_display_value', 'all');
|
|
112
|
-
params.set('sysparm_fields', 'sys_id,name,state,application,application.scope,parent,sys_created_on,sys_updated_on');
|
|
242
|
+
params.set('sysparm_fields', 'sys_id,name,state,application,application.scope,parent,description,sys_created_on,sys_updated_on');
|
|
113
243
|
const recs = await app.sdk.list('sys_update_set', params);
|
|
114
244
|
if (Array.isArray(recs) && recs.length > 0) full = recs[0];
|
|
115
245
|
} catch {
|
|
@@ -123,6 +253,7 @@ export async function formatUpdateSetDetail(app, record) {
|
|
|
123
253
|
const parent = getStringField(full, 'parent') || '';
|
|
124
254
|
const created = getStringField(full, 'sys_created_on') || '';
|
|
125
255
|
const updated = getStringField(full, 'sys_updated_on') || '';
|
|
256
|
+
const description = getStringField(full, 'description');
|
|
126
257
|
|
|
127
258
|
// Child updates: filename only, newest first by sys_updated_on.
|
|
128
259
|
// Also group by element type (sys_class_name) and flag risky types for
|
|
@@ -170,6 +301,7 @@ export async function formatUpdateSetDetail(app, record) {
|
|
|
170
301
|
// children are best-effort — a broken query shouldn't kill the display
|
|
171
302
|
}
|
|
172
303
|
|
|
304
|
+
const hierarchyData = await buildUpdateSetHierarchy(app, full);
|
|
173
305
|
const lines = [];
|
|
174
306
|
lines.push(`Update set: ${name}`);
|
|
175
307
|
lines.push(` State: ${state}`);
|
|
@@ -177,7 +309,10 @@ export async function formatUpdateSetDetail(app, record) {
|
|
|
177
309
|
if (parent) lines.push(` Parent: ${parent}`);
|
|
178
310
|
if (created) lines.push(` Created: ${created}`);
|
|
179
311
|
if (updated) lines.push(` Updated: ${updated}`);
|
|
180
|
-
lines.push(`
|
|
312
|
+
if (description) lines.push(` Description: ${description}`);
|
|
313
|
+
lines.push(...renderUpdateSetHierarchy(hierarchyData.hierarchy, hierarchyData.cycleIds, hierarchyData.missingParents));
|
|
314
|
+
lines.push('Payload / update XML:');
|
|
315
|
+
lines.push(` Updates: ${children.length}`);
|
|
181
316
|
const typeEntries = Object.entries(byType).sort((a, b) => b[1] - a[1]);
|
|
182
317
|
if (typeEntries.length > 0) {
|
|
183
318
|
lines.push(` By type: ${typeEntries.map(([t, n]) => `${t}×${n}`).join(', ')}`);
|
|
@@ -243,6 +378,11 @@ export async function formatUpdateSetDetail(app, record) {
|
|
|
243
378
|
parent,
|
|
244
379
|
sys_created_on: created,
|
|
245
380
|
sys_updated_on: updated,
|
|
381
|
+
description,
|
|
382
|
+
scope: application,
|
|
383
|
+
hierarchy: hierarchyData.hierarchy,
|
|
384
|
+
hierarchy_cycles: hierarchyData.cycleIds,
|
|
385
|
+
hierarchy_missing_parents: hierarchyData.missingParents,
|
|
246
386
|
children,
|
|
247
387
|
by_type: byType,
|
|
248
388
|
risky_count: risky.length,
|
package/src/config.js
CHANGED
|
@@ -29,11 +29,17 @@ export function cacheDir() {
|
|
|
29
29
|
export function normalizeInstanceURL(url) {
|
|
30
30
|
if (!url) return '';
|
|
31
31
|
url = url.trim();
|
|
32
|
-
url = url.replace(/\/$/, '');
|
|
33
32
|
if (!/^https?:\/\//i.test(url)) {
|
|
34
33
|
url = 'https://' + url;
|
|
35
34
|
}
|
|
36
|
-
|
|
35
|
+
try {
|
|
36
|
+
// Profiles store the instance origin, never a page, API route, query, or
|
|
37
|
+
// fragment pasted from the browser.
|
|
38
|
+
return new URL(url).origin;
|
|
39
|
+
} catch {
|
|
40
|
+
// Preserve the old permissive behavior for non-standard local endpoints.
|
|
41
|
+
return url.replace(/\/$/, '');
|
|
42
|
+
}
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
function loadFromFile(cfg, filePath, source) {
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# pre-commit-check.sh - Run this before committing to check architecture compliance
|
|
3
|
-
|
|
4
|
-
echo "🔍 Checking architecture patterns..."
|
|
5
|
-
|
|
6
|
-
# Check for forbidden SDK patterns
|
|
7
|
-
FORBIDDEN_PATTERNS=(
|
|
8
|
-
"func.*Client.*ListForm"
|
|
9
|
-
"func.*Client.*ListList"
|
|
10
|
-
"func.*Client.*GetSP"
|
|
11
|
-
"func.*Client.*ListSP"
|
|
12
|
-
)
|
|
13
|
-
|
|
14
|
-
ERRORS=0
|
|
15
|
-
|
|
16
|
-
for pattern in "${FORBIDDEN_PATTERNS[@]}"; do
|
|
17
|
-
matches=$(grep -r "$pattern" internal/sdk/*.go 2>/dev/null | grep -v "_test.go" | grep -v "^Binary")
|
|
18
|
-
if [ ! -z "$matches" ]; then
|
|
19
|
-
echo "❌ Found forbidden SDK pattern: $pattern"
|
|
20
|
-
echo "$matches"
|
|
21
|
-
ERRORS=$((ERRORS + 1))
|
|
22
|
-
fi
|
|
23
|
-
done
|
|
24
|
-
|
|
25
|
-
# Check that commands don't import old SDK types
|
|
26
|
-
BAD_IMPORTS=(
|
|
27
|
-
"sdk.FormSection"
|
|
28
|
-
"sdk.FormElement"
|
|
29
|
-
"sdk.ListLayout"
|
|
30
|
-
"sdk.ListElement"
|
|
31
|
-
"sdk.SPPage"
|
|
32
|
-
"sdk.SPWidgetInstance"
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
for pattern in "${BAD_IMPORTS[@]}"; do
|
|
36
|
-
matches=$(grep -r "$pattern" internal/commands/**/*.go 2>/dev/null)
|
|
37
|
-
if [ ! -z "$matches" ]; then
|
|
38
|
-
echo "❌ Command using SDK type instead of local type: $pattern"
|
|
39
|
-
echo "$matches"
|
|
40
|
-
ERRORS=$((ERRORS + 1))
|
|
41
|
-
fi
|
|
42
|
-
done
|
|
43
|
-
|
|
44
|
-
# Run architecture tests
|
|
45
|
-
echo "🧪 Running architecture tests..."
|
|
46
|
-
if ! go test ./internal/sdk -run TestNoSDKHelperMethods -run TestCommandsUseDirectSDKList > /dev/null 2>&1; then
|
|
47
|
-
echo "❌ Architecture tests failed"
|
|
48
|
-
go test ./internal/sdk -v -run "TestNoSDKHelperMethods|TestCommandsUseDirectSDKList"
|
|
49
|
-
ERRORS=$((ERRORS + 1))
|
|
50
|
-
fi
|
|
51
|
-
|
|
52
|
-
if [ $ERRORS -eq 0 ]; then
|
|
53
|
-
echo "✅ All architecture checks passed!"
|
|
54
|
-
exit 0
|
|
55
|
-
else
|
|
56
|
-
echo ""
|
|
57
|
-
echo "⚠️ Architecture violations found!"
|
|
58
|
-
echo " Remember: Commands should call app.SDK.List() directly with local types."
|
|
59
|
-
echo " See internal/commands/forms.go for the correct pattern."
|
|
60
|
-
exit 1
|
|
61
|
-
fi
|
package/scripts/release.js
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { execSync } from 'child_process';
|
|
3
|
-
import { readFileSync } from 'fs';
|
|
4
|
-
import { fileURLToPath } from 'url';
|
|
5
|
-
import { dirname, join } from 'path';
|
|
6
|
-
|
|
7
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
|
|
9
|
-
|
|
10
|
-
const type = process.argv[2];
|
|
11
|
-
if (!type || !['patch', 'minor', 'major'].includes(type)) {
|
|
12
|
-
console.error('Usage: npm run release -- <patch|minor|major>');
|
|
13
|
-
process.exit(1);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
let branch;
|
|
17
|
-
try {
|
|
18
|
-
branch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8' }).trim();
|
|
19
|
-
} catch {
|
|
20
|
-
console.error('Error: Not in a git repository');
|
|
21
|
-
process.exit(1);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (branch !== 'main') {
|
|
25
|
-
console.error(`Error: Releases can only be made from 'main' branch (currently on '${branch}')`);
|
|
26
|
-
process.exit(1);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const prefix = 'v';
|
|
30
|
-
|
|
31
|
-
console.log('');
|
|
32
|
-
console.log(`🚀 Releasing from '${branch}'`);
|
|
33
|
-
console.log(` Bump: ${type}`);
|
|
34
|
-
console.log('');
|
|
35
|
-
|
|
36
|
-
// Run tests
|
|
37
|
-
console.log('⏳ Running tests...');
|
|
38
|
-
try {
|
|
39
|
-
execSync('npm test', { stdio: 'inherit' });
|
|
40
|
-
} catch {
|
|
41
|
-
console.error('');
|
|
42
|
-
console.error('❌ Tests failed. Release aborted.');
|
|
43
|
-
process.exit(1);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Bump version with correct tag prefix
|
|
47
|
-
console.log('');
|
|
48
|
-
console.log(`⏳ Bumping version (${type})...`);
|
|
49
|
-
execSync(`npm version ${type} --tag-version-prefix=${prefix}`, { stdio: 'inherit' });
|
|
50
|
-
|
|
51
|
-
// Push commits and tags
|
|
52
|
-
console.log('');
|
|
53
|
-
console.log('⏳ Pushing to origin...');
|
|
54
|
-
execSync('git push && git push --tags', { stdio: 'inherit' });
|
|
55
|
-
|
|
56
|
-
// Show final state
|
|
57
|
-
const newPkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
|
|
58
|
-
console.log('');
|
|
59
|
-
console.log('✅ Release complete!');
|
|
60
|
-
console.log(` Version: ${newPkg.version}`);
|
|
61
|
-
console.log(` Tag: ${prefix}${newPkg.version}`);
|
|
62
|
-
console.log('');
|
package/scripts/tag-reminder.js
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { execSync } from 'child_process';
|
|
3
|
-
import { readFileSync } from 'fs';
|
|
4
|
-
import { fileURLToPath } from 'url';
|
|
5
|
-
import { dirname, join } from 'path';
|
|
6
|
-
|
|
7
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
|
|
9
|
-
|
|
10
|
-
let branch;
|
|
11
|
-
try {
|
|
12
|
-
branch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8' }).trim();
|
|
13
|
-
} catch {
|
|
14
|
-
branch = 'unknown';
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const version = pkg.version;
|
|
18
|
-
const prefix = 'v';
|
|
19
|
-
|
|
20
|
-
console.log('');
|
|
21
|
-
console.log('📦 Release Reminder');
|
|
22
|
-
console.log('');
|
|
23
|
-
console.log(` Current version: ${version}`);
|
|
24
|
-
console.log(` Current branch: ${branch}`);
|
|
25
|
-
console.log('');
|
|
26
|
-
console.log(' To create a release tag, run:');
|
|
27
|
-
console.log('');
|
|
28
|
-
console.log(` git tag ${prefix}${version}`);
|
|
29
|
-
console.log(' git push origin --tags');
|
|
30
|
-
console.log('');
|