@nasebanal/cli 0.4.0 → 0.6.0

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
@@ -10,7 +10,7 @@ Contract-Driven Development (CDD) pipeline, alongside the web frontends.
10
10
 
11
11
  ```console
12
12
  $ nb account me get
13
- $ nb recorder records list
13
+ $ nb record records list
14
14
  $ nb target shares accept <id>
15
15
  $ nb account me tokens create --data '{"name":"ci"}'
16
16
  ```
@@ -117,6 +117,20 @@ needed for a registry install.
117
117
  > `api-specs-*` packages), it captures this package too. Install with
118
118
  > `npm install -g @nasebanal/cli --@nasebanal:registry=https://registry.npmjs.org/`.
119
119
 
120
+ ### Upgrading
121
+
122
+ ```bash
123
+ nb upgrade # install the latest published version
124
+ nb upgrade --check # only report whether a newer version exists
125
+ ```
126
+
127
+ When a newer version is out, interactive runs print a one-line notice on stderr
128
+ (`Update available: 0.5.0 -> 0.6.0`). The registry is checked at most once a day
129
+ and the result is cached in `~/.config/nasebanal/update-check.json`. Nothing is
130
+ printed when stderr is not a terminal or `CI` is set; `NB_NO_UPDATE_NOTIFIER=1`
131
+ turns the notice off. A dev link to a source checkout is never overwritten:
132
+ `nb upgrade` tells you to `git pull && npm run build` instead.
133
+
120
134
  ### Versioning and releases
121
135
 
122
136
  Versions follow SemVer (0.x until 1.0: breaking changes bump the minor). Each
package/dist/apis.js CHANGED
@@ -1,7 +1,6 @@
1
1
  export const API_CATALOG = [
2
2
  { name: "account", spec: "account.yaml", summary: "User profiles, subscriptions, billing, and API tokens" },
3
- { name: "recorder", spec: "recorder.yaml", summary: "Records and tags (NASEBANAL Recorder)" },
3
+ { name: "record", spec: "recorder.yaml", summary: "Records and tags (NASEBANAL Recorder)" },
4
4
  { name: "target", spec: "target.yaml", summary: "Targets, cells, and shares (NASEBANAL Target)" },
5
5
  { name: "assurance", spec: "assurance.yaml", summary: "Monitors, projects and CI/CD test results (NASEBANAL Assurance)" },
6
- { name: "app-template", spec: "app-template.yaml", summary: "Reference API surface (App Template)" },
7
6
  ];
package/dist/config.js CHANGED
@@ -12,7 +12,7 @@ import { homedir } from "node:os";
12
12
  import { join } from "node:path";
13
13
  import { mkdirSync, readFileSync, writeFileSync, chmodSync, existsSync } from "node:fs";
14
14
  import { refreshTokens } from "./oauth.js";
15
- function configDir() {
15
+ export function configDir() {
16
16
  const base = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
17
17
  return join(base, "nasebanal");
18
18
  }
package/dist/index.js CHANGED
@@ -14,6 +14,7 @@ import { buildAuthCommand } from "./auth.js";
14
14
  import { specExists } from "./spec/loader.js";
15
15
  import { printError } from "./output.js";
16
16
  import { buildReportUploadCommand } from "./reports/upload.js";
17
+ import { buildUpgradeCommand, startUpdateNotifier } from "./update.js";
17
18
  // Single source of truth for the version: package.json (dist/ and src/ both sit
18
19
  // next to it). A hard-coded string here drifts from the release tag.
19
20
  const { version } = createRequire(import.meta.url)("../package.json");
@@ -26,6 +27,7 @@ program
26
27
  .option("--token <token>", "override the stored PAT for this invocation")
27
28
  .showHelpAfterError();
28
29
  program.addCommand(buildAuthCommand());
30
+ program.addCommand(buildUpgradeCommand(version));
29
31
  for (const api of API_CATALOG) {
30
32
  if (!specExists(api.spec)) {
31
33
  // Spec not synced yet — register a stub that explains how to fix it rather
@@ -50,8 +52,10 @@ for (const api of API_CATALOG) {
50
52
  program.addCommand(group);
51
53
  }
52
54
  async function main() {
55
+ const notifier = startUpdateNotifier(version, process.argv);
53
56
  try {
54
57
  await program.parseAsync(process.argv);
58
+ await notifier.refresh();
55
59
  }
56
60
  catch (err) {
57
61
  printError(err);
package/dist/update.js ADDED
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Self-update: a passive "new version available" notice and `nb upgrade`.
3
+ *
4
+ * Follows the usual CLI convention (npm, gh, gcloud): the notice is a single
5
+ * line on stderr, shown at most once a day from a cached registry lookup, and
6
+ * silent for non-interactive use (pipes, CI) so scripted output never changes.
7
+ * `NB_NO_UPDATE_NOTIFIER=1` turns it off.
8
+ */
9
+ import { spawnSync } from "node:child_process";
10
+ import { mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { Command } from "commander";
13
+ import { configDir } from "./config.js";
14
+ const PACKAGE = "@nasebanal/cli";
15
+ const REGISTRY = "https://registry.npmjs.org";
16
+ const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
17
+ const FETCH_TIMEOUT_MS = 1500;
18
+ /** 1 if a > b, -1 if a < b, 0 if equal (plain `x.y.z`; pre-release tags are ignored). */
19
+ export function compareVersions(a, b) {
20
+ const pa = a.split("-")[0].split(".").map(Number);
21
+ const pb = b.split("-")[0].split(".").map(Number);
22
+ for (let i = 0; i < 3; i++) {
23
+ const d = (pa[i] || 0) - (pb[i] || 0);
24
+ if (d !== 0)
25
+ return d > 0 ? 1 : -1;
26
+ }
27
+ return 0;
28
+ }
29
+ function cachePath() {
30
+ return join(configDir(), "update-check.json");
31
+ }
32
+ function readCache() {
33
+ try {
34
+ return JSON.parse(readFileSync(cachePath(), "utf8"));
35
+ }
36
+ catch {
37
+ return undefined;
38
+ }
39
+ }
40
+ function writeCache(cache) {
41
+ try {
42
+ mkdirSync(configDir(), { recursive: true });
43
+ writeFileSync(cachePath(), JSON.stringify(cache) + "\n");
44
+ }
45
+ catch {
46
+ /* best effort */
47
+ }
48
+ }
49
+ /** Latest published version. Goes to the public registry directly, so a `~/.npmrc` scope mapping cannot hide it. */
50
+ export async function fetchLatest(timeoutMs = FETCH_TIMEOUT_MS) {
51
+ try {
52
+ const res = await fetch(`${REGISTRY}/${PACKAGE.replace("/", "%2F")}/latest`, {
53
+ signal: AbortSignal.timeout(timeoutMs),
54
+ });
55
+ if (!res.ok)
56
+ return undefined;
57
+ return (await res.json()).version;
58
+ }
59
+ catch {
60
+ return undefined;
61
+ }
62
+ }
63
+ function notifierEnabled() {
64
+ return !process.env.NB_NO_UPDATE_NOTIFIER && !process.env.CI && !!process.stderr.isTTY;
65
+ }
66
+ /**
67
+ * Arrange for the update notice to print when the process exits. Reads the
68
+ * cache synchronously; when it is older than a day, refreshes it afterwards
69
+ * (bounded by a short timeout) so the notice appears from the next run on.
70
+ */
71
+ export function startUpdateNotifier(current, argv) {
72
+ const noop = { refresh: async () => { } };
73
+ if (!notifierEnabled() || argv.slice(2).some((a) => a === "upgrade" || a === "--version" || a === "-V"))
74
+ return noop;
75
+ const cache = readCache();
76
+ if (cache?.latest && compareVersions(cache.latest, current) > 0) {
77
+ const latest = cache.latest;
78
+ process.on("exit", () => {
79
+ process.stderr.write(`\nUpdate available: ${current} -> ${latest}. Run \`nb upgrade\` to install it.\n`);
80
+ });
81
+ }
82
+ const stale = !cache || Date.now() - cache.checkedAt > CHECK_INTERVAL_MS;
83
+ return {
84
+ refresh: async () => {
85
+ if (!stale)
86
+ return;
87
+ const latest = await fetchLatest();
88
+ // Record the attempt even on failure so an offline machine is not retried on every command.
89
+ writeCache({ checkedAt: Date.now(), latest: latest ?? cache?.latest });
90
+ },
91
+ };
92
+ }
93
+ /** True when `nb` is a dev link to a checkout rather than an npm-installed copy. */
94
+ function isLinkedCheckout() {
95
+ try {
96
+ return !realpathSync(process.argv[1]).includes("node_modules");
97
+ }
98
+ catch {
99
+ return false;
100
+ }
101
+ }
102
+ export function buildUpgradeCommand(current) {
103
+ return new Command("upgrade")
104
+ .description("Upgrade nb to the latest published version")
105
+ .option("--check", "only report whether a newer version exists")
106
+ .action(async (opts) => {
107
+ const latest = await fetchLatest(5000);
108
+ if (!latest)
109
+ throw new Error("Could not reach the npm registry to look up the latest version.");
110
+ writeCache({ checkedAt: Date.now(), latest });
111
+ if (compareVersions(latest, current) <= 0) {
112
+ process.stdout.write(`nb ${current} is up to date.\n`);
113
+ return;
114
+ }
115
+ process.stdout.write(`Update available: ${current} -> ${latest}\n`);
116
+ if (opts.check)
117
+ return;
118
+ if (isLinkedCheckout()) {
119
+ process.stdout.write("This nb is linked to a source checkout: `git pull && npm run build` there instead.\n");
120
+ return;
121
+ }
122
+ const r = spawnSync("npm", ["install", "-g", `${PACKAGE}@${latest}`, `--@nasebanal:registry=${REGISTRY}/`], { stdio: "inherit" });
123
+ if (r.status !== 0) {
124
+ throw new Error(`npm install failed. Run it yourself: npm install -g ${PACKAGE}@latest --@nasebanal:registry=${REGISTRY}/`);
125
+ }
126
+ });
127
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nasebanal/cli",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "gcloud-style command-line interface for NASEBANAL APIs, generated from nb-api-specs OpenAPI contracts.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,488 +0,0 @@
1
- openapi: 3.0.3
2
- info:
3
- title: NASEBANAL - App Template API
4
- description: |-
5
- A template API for NASEBANAL applications. Provides sample CRUD endpoints, user authentication via Auth0 JWT and Personal Access Tokens (PAT), and integration with the Account API.
6
-
7
- All responses use a uniform envelope: successful responses are `{ "success": true, "data": ... }` and failures are `{ "success": false, "error": { "code", "message" } }`. This mirrors `successResponse()` / `errorResponse()` in the implementation. The `success` discriminator is pinned with `enum` so both Specmatic stub generation and contract tests treat it as a fixed value per response class.
8
- version: 1.1.1
9
- contact:
10
- name: NASEBANAL
11
- url: https://www.nasebanal.com
12
- servers:
13
- - url: http://localhost:8789
14
- description: Local dev (wrangler dev)
15
- - url: https://api.app-template.nasebanal.com
16
- description: Production
17
- security:
18
- - BearerAuth: []
19
- paths:
20
- /api/v1/health:
21
- get:
22
- operationId: getHealth
23
- tags:
24
- - Health
25
- summary: Health check
26
- description: Returns the health status of the API.
27
- security: []
28
- responses:
29
- "200":
30
- description: API is healthy
31
- content:
32
- application/json:
33
- schema:
34
- type: object
35
- required:
36
- - success
37
- - data
38
- properties:
39
- success:
40
- type: boolean
41
- enum:
42
- - true
43
- data:
44
- type: object
45
- required:
46
- - status
47
- - timestamp
48
- properties:
49
- status:
50
- type: string
51
- timestamp:
52
- type: string
53
- format: date-time
54
- examples:
55
- valid:
56
- value:
57
- success: true
58
- data:
59
- status: ok
60
- timestamp: 2026-05-14T12:00:00Z
61
- /api/v1/me:
62
- get:
63
- operationId: getCurrentUser
64
- tags:
65
- - User
66
- summary: Get current user profile
67
- description: Returns the authenticated user's profile information from the Account API.
68
- responses:
69
- "200":
70
- description: User profile
71
- content:
72
- application/json:
73
- schema:
74
- $ref: "#/components/schemas/UserResponse"
75
- examples:
76
- valid:
77
- value:
78
- success: true
79
- data:
80
- id: 42
81
- auth0_sub: auth0|0123456789abcdef
82
- email: user@example.com
83
- name: Sample User
84
- language: ja
85
- plan: pro
86
- subscription_status: active
87
- subscription_expires_at: 2026-12-31T23:59:59Z
88
- "401":
89
- $ref: "#/components/responses/Unauthorized"
90
- /api/v1/items:
91
- get:
92
- operationId: listItems
93
- tags:
94
- - Items
95
- summary: List items
96
- description: Returns all items belonging to the authenticated user.
97
- responses:
98
- "200":
99
- description: List of items
100
- content:
101
- application/json:
102
- schema:
103
- type: object
104
- required:
105
- - success
106
- - data
107
- properties:
108
- success:
109
- type: boolean
110
- enum:
111
- - true
112
- data:
113
- type: object
114
- required:
115
- - data
116
- - total
117
- properties:
118
- data:
119
- type: array
120
- items:
121
- $ref: "#/components/schemas/Item"
122
- total:
123
- type: integer
124
- examples:
125
- valid:
126
- value:
127
- success: true
128
- data:
129
- data:
130
- - id: itm_a1b2c3d4
131
- user_id: 42
132
- name: My Item
133
- description: A sample item description
134
- created_at: 2026-05-14T12:00:00Z
135
- updated_at: 2026-05-14T12:00:00Z
136
- total: 1
137
- "401":
138
- $ref: "#/components/responses/Unauthorized"
139
- post:
140
- operationId: createItem
141
- tags:
142
- - Items
143
- summary: Create item
144
- description: Creates a new item for the authenticated user.
145
- requestBody:
146
- required: true
147
- content:
148
- application/json:
149
- schema:
150
- $ref: "#/components/schemas/CreateItemInput"
151
- examples:
152
- valid:
153
- value:
154
- name: My Item
155
- description: A sample item description
156
- responses:
157
- "201":
158
- description: Item created
159
- content:
160
- application/json:
161
- schema:
162
- $ref: "#/components/schemas/ItemResponse"
163
- examples:
164
- valid:
165
- value:
166
- success: true
167
- data:
168
- id: itm_a1b2c3d4
169
- user_id: 42
170
- name: My Item
171
- description: A sample item description
172
- created_at: 2026-05-14T12:00:00Z
173
- updated_at: 2026-05-14T12:00:00Z
174
- "401":
175
- $ref: "#/components/responses/Unauthorized"
176
- /api/v1/items/{id}:
177
- get:
178
- operationId: getItem
179
- tags:
180
- - Items
181
- summary: Get item
182
- description: Returns a single item by ID.
183
- parameters:
184
- - name: id
185
- in: path
186
- required: true
187
- schema:
188
- type: string
189
- examples:
190
- valid:
191
- value: itm_a1b2c3d4
192
- responses:
193
- "200":
194
- description: Item details
195
- content:
196
- application/json:
197
- schema:
198
- $ref: "#/components/schemas/ItemResponse"
199
- examples:
200
- valid:
201
- value:
202
- success: true
203
- data:
204
- id: itm_a1b2c3d4
205
- user_id: 42
206
- name: My Item
207
- description: A sample item description
208
- created_at: 2026-05-14T12:00:00Z
209
- updated_at: 2026-05-14T12:00:00Z
210
- "401":
211
- $ref: "#/components/responses/Unauthorized"
212
- "404":
213
- $ref: "#/components/responses/NotFound"
214
- put:
215
- operationId: updateItem
216
- tags:
217
- - Items
218
- summary: Update item
219
- description: Updates an existing item.
220
- parameters:
221
- - name: id
222
- in: path
223
- required: true
224
- schema:
225
- type: string
226
- examples:
227
- valid:
228
- value: itm_a1b2c3d4
229
- requestBody:
230
- required: true
231
- content:
232
- application/json:
233
- schema:
234
- $ref: "#/components/schemas/UpdateItemInput"
235
- examples:
236
- valid:
237
- value:
238
- name: Updated Item
239
- description: An updated item description
240
- responses:
241
- "200":
242
- description: Item updated
243
- content:
244
- application/json:
245
- schema:
246
- $ref: "#/components/schemas/ItemResponse"
247
- examples:
248
- valid:
249
- value:
250
- success: true
251
- data:
252
- id: itm_a1b2c3d4
253
- user_id: 42
254
- name: Updated Item
255
- description: An updated item description
256
- created_at: 2026-05-14T12:00:00Z
257
- updated_at: 2026-05-15T09:30:00Z
258
- "401":
259
- $ref: "#/components/responses/Unauthorized"
260
- "404":
261
- $ref: "#/components/responses/NotFound"
262
- delete:
263
- operationId: deleteItem
264
- tags:
265
- - Items
266
- summary: Delete item
267
- description: Deletes an item by ID.
268
- parameters:
269
- - name: id
270
- in: path
271
- required: true
272
- schema:
273
- type: string
274
- examples:
275
- valid:
276
- value: itm_a1b2c3d4
277
- responses:
278
- "200":
279
- description: Item deleted
280
- content:
281
- application/json:
282
- schema:
283
- $ref: "#/components/schemas/SuccessResponse"
284
- examples:
285
- valid:
286
- value:
287
- success: true
288
- data:
289
- success: true
290
- "401":
291
- $ref: "#/components/responses/Unauthorized"
292
- "404":
293
- $ref: "#/components/responses/NotFound"
294
- components:
295
- securitySchemes:
296
- BearerAuth:
297
- type: http
298
- scheme: bearer
299
- bearerFormat: JWT or PAT
300
- description: Use either an Auth0 JWT token or a Personal Access Token (PAT) starting with `nbpat_`.
301
- schemas:
302
- User:
303
- type: object
304
- required:
305
- - id
306
- - auth0_sub
307
- - email
308
- - name
309
- - language
310
- - plan
311
- properties:
312
- id:
313
- type: integer
314
- auth0_sub:
315
- type: string
316
- email:
317
- type: string
318
- format: email
319
- name:
320
- type: string
321
- language:
322
- type: string
323
- enum:
324
- - ja
325
- - en
326
- plan:
327
- type: string
328
- enum:
329
- - free
330
- - pro
331
- - family
332
- - enterprise
333
- subscription_status:
334
- type: string
335
- enum:
336
- - active
337
- - canceled
338
- - past_due
339
- - trialing
340
- - null
341
- nullable: true
342
- subscription_expires_at:
343
- type: string
344
- format: date-time
345
- nullable: true
346
- UserResponse:
347
- type: object
348
- required:
349
- - success
350
- - data
351
- properties:
352
- success:
353
- type: boolean
354
- enum:
355
- - true
356
- data:
357
- $ref: "#/components/schemas/User"
358
- Item:
359
- type: object
360
- required:
361
- - id
362
- - user_id
363
- - name
364
- - created_at
365
- - updated_at
366
- properties:
367
- id:
368
- type: string
369
- user_id:
370
- type: integer
371
- name:
372
- type: string
373
- description:
374
- type: string
375
- nullable: true
376
- created_at:
377
- type: string
378
- format: date-time
379
- updated_at:
380
- type: string
381
- format: date-time
382
- ItemResponse:
383
- type: object
384
- required:
385
- - success
386
- - data
387
- properties:
388
- success:
389
- type: boolean
390
- enum:
391
- - true
392
- data:
393
- $ref: "#/components/schemas/Item"
394
- CreateItemInput:
395
- type: object
396
- required:
397
- - name
398
- properties:
399
- name:
400
- type: string
401
- minLength: 1
402
- maxLength: 200
403
- description:
404
- type: string
405
- maxLength: 1000
406
- UpdateItemInput:
407
- type: object
408
- properties:
409
- name:
410
- type: string
411
- minLength: 1
412
- maxLength: 200
413
- description:
414
- type: string
415
- maxLength: 1000
416
- SuccessResponse:
417
- type: object
418
- required:
419
- - success
420
- - data
421
- properties:
422
- success:
423
- type: boolean
424
- enum:
425
- - true
426
- data:
427
- type: object
428
- required:
429
- - success
430
- properties:
431
- success:
432
- type: boolean
433
- enum:
434
- - true
435
- Error:
436
- type: object
437
- required:
438
- - success
439
- - error
440
- properties:
441
- success:
442
- type: boolean
443
- enum:
444
- - false
445
- error:
446
- type: object
447
- required:
448
- - code
449
- - message
450
- properties:
451
- code:
452
- type: string
453
- message:
454
- type: string
455
- responses:
456
- Unauthorized:
457
- description: Authentication required or invalid token
458
- content:
459
- application/json:
460
- schema:
461
- $ref: "#/components/schemas/Error"
462
- examples:
463
- unauthorized:
464
- value:
465
- success: false
466
- error:
467
- code: AUTH_ERROR
468
- message: No authentication token provided
469
- NotFound:
470
- description: Resource not found
471
- content:
472
- application/json:
473
- schema:
474
- $ref: "#/components/schemas/Error"
475
- examples:
476
- not_found:
477
- value:
478
- success: false
479
- error:
480
- code: NOT_FOUND
481
- message: Resource not found
482
- tags:
483
- - name: Health
484
- description: Health check endpoints
485
- - name: User
486
- description: User profile endpoints
487
- - name: Items
488
- description: Sample CRUD endpoints (replace with your domain)