strckro 0.2.0 → 0.3.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/DOCUMENTATION.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Strckro CLI — เอกสารการใช้งาน
2
2
 
3
- เวอร์ชัน CLI: `0.2.0`
3
+ เวอร์ชัน CLI: `0.3.0`
4
4
  คำสั่ง: `skctl`
5
5
  อัปเดตเอกสาร: 6 พฤศจิกายน 2026
6
6
 
@@ -47,10 +47,10 @@ npx --package strckro skctl --help
47
47
  npm run pack:release
48
48
  ```
49
49
 
50
- ส่ง `strckro-0.2.0.tgz` ไปยังเครื่องอื่นแล้วรัน:
50
+ ส่ง `strckro-0.3.0.tgz` ไปยังเครื่องอื่นแล้วรัน:
51
51
 
52
52
  ```sh
53
- npm install --global ./strckro-0.2.0.tgz
53
+ npm install --global ./strckro-0.3.0.tgz
54
54
  skctl --version
55
55
  ```
56
56
 
@@ -188,15 +188,28 @@ skctl init --project my-app
188
188
 
189
189
  Credential ที่บันทึกไว้จะใช้กับ server origin เดิมเท่านั้น สามารถใช้ `STRCKRO_TOKEN` สำหรับ CI โดยไม่บันทึก credential ลงเครื่อง
190
190
 
191
+ **token ไปได้เฉพาะ server ที่คุณเลือกเอง:** `strckro.json` เป็นไฟล์ใน repository ซึ่งอาจมาจากคนอื่น (repo ที่ clone มา, fork ใน CI, repo ที่ AI agent เปิด) CLI จึงไม่ส่ง token ที่คุณให้ (`STRCKRO_TOKEN`, `login --token`, `login --token-stdin`) ไปยัง server ที่ระบุอยู่ใน `strckro.json` เพียงอย่างเดียว Token จะถูกส่งเมื่อ server มาจาก `--server`, `STRCKRO_SERVER` หรือเป็น origin เดียวกับที่ login ไว้บนเครื่องนี้ มิฉะนั้นคำสั่งจะหยุดด้วย exit 2 โดยยังไม่ส่งคำขอใดออกไป CI ที่ใช้ `STRCKRO_TOKEN` ให้ตั้ง `STRCKRO_SERVER` คู่กันเสมอ เช่นเดียวกับตัวอย่างในหัวข้อ CI
192
+
191
193
  ## 6. สร้างและซิงก์โปรเจค
192
194
 
193
195
  ### ดูรายการและสถานะ
194
196
 
195
197
  ```sh
198
+ skctl projects list
199
+ skctl project my-app --info
200
+ skctl project --info
196
201
  skctl projects list --json
197
- skctl projects status my-app --json
202
+ skctl project my-app --info --json
198
203
  ```
199
204
 
205
+ `projects list` แสดงเฉพาะ slug, ประเภทงาน (`web`/`worker`), runtime, deployment และ process หนึ่งบรรทัดต่อโปรเจค ไม่แสดง repository, release history หรือ logs ในรายการ
206
+
207
+ `project [SLUG] --info` แสดงข้อมูลทีละโปรเจค: ชื่อ, source, branch/directory, sync, active/previous release, งาน deploy และ domains หากไม่ระบุ slug จะใช้ลำดับเลือกโปรเจคในหัวข้อ 5 ส่วน `project [SLUG]` แสดงตารางสรุปเฉพาะโปรเจคนั้น
208
+
209
+ Deployment `active` แสดงสถานะ release ที่บันทึกไว้ ส่วน process เป็นผล runtime probe แยกกัน `unknown` หมายถึงยังยืนยันสถานะ process ไม่ได้ จึงไม่ควรใช้ deployment state เพียงอย่างเดียวสรุปว่าแอป healthy
210
+
211
+ เติม `--json` เพื่อรับข้อมูล API ครบเหมือนเดิม รวม release history และ metadata ที่ token มีสิทธิ์อ่าน โดย stdout เป็น JSON object เดียว ไม่มีตารางหรือข้อความแนะนำ `skctl projects status [SLUG] --json` ยังคงใช้ได้ตามเดิม และให้โครงสร้างเดียวกับ `skctl project [SLUG] --info --json`
212
+
200
213
  ### สร้างหรืออัปเดตจากไฟล์
201
214
 
202
215
  สร้าง `project.json` เช่น:
@@ -410,7 +423,7 @@ Token ไม่สามารถจัดการบัญชี/สิทธ
410
423
  | Environment variable | ความหมาย |
411
424
  | --- | --- |
412
425
  | `STRCKRO_SERVER` | Origin ของ Portal |
413
- | `STRCKRO_TOKEN` | Token สำหรับ process/CI โดยไม่ต้อง login บันทึกลงเครื่อง |
426
+ | `STRCKRO_TOKEN` | Token สำหรับ process/CI โดยไม่ต้อง login บันทึกลงเครื่อง ใช้คู่กับ `STRCKRO_SERVER` หรือ `--server` (ไม่ส่งไปยัง server ที่มาจาก `strckro.json` อย่างเดียว) |
414
427
  | `STRCKRO_PROJECT` | Default project slug |
415
428
  | `STRCKRO_CONFIG_DIR` | เปลี่ยนโฟลเดอร์เก็บ credential |
416
429
  | `XDG_CONFIG_HOME` | Base config directory หากไม่ได้กำหนด `STRCKRO_CONFIG_DIR` |
package/PUBLISHING.md CHANGED
@@ -5,6 +5,8 @@
5
5
  Registry: `https://registry.npmjs.org/`
6
6
  การเข้าถึง: public
7
7
 
8
+ รุ่น 0.3.0 เปลี่ยน `projects list` เป็นตารางสรุปหนึ่งบรรทัดต่อโปรเจค เพิ่ม `project [SLUG] --info` สำหรับเจาะรายละเอียด และคงข้อมูล `--json` กับคำสั่ง `projects status` เดิมไว้
9
+
8
10
  ## ตรวจแพ็กเกจก่อนเผยแพร่
9
11
 
10
12
  ```sh
@@ -53,10 +55,10 @@ skctl --version
53
55
  npm run pack:release
54
56
  ```
55
57
 
56
- จะได้ `strckro-0.2.0.tgz` ตามเวอร์ชันปัจจุบัน ส่งไฟล์นี้ไปยังเครื่องอื่นแล้วติดตั้งได้ทันที:
58
+ จะได้ `strckro-0.3.0.tgz` ตามเวอร์ชันปัจจุบัน ส่งไฟล์นี้ไปยังเครื่องอื่นแล้วติดตั้งได้ทันที:
57
59
 
58
60
  ```sh
59
- npm install --global ./strckro-0.2.0.tgz
61
+ npm install --global ./strckro-0.3.0.tgz
60
62
  skctl --version
61
63
  skctl login --server https://portal.example.com --no-browser
62
64
  ```
package/README.md CHANGED
@@ -23,7 +23,7 @@ npx --package strckro skctl --help
23
23
  ```sh
24
24
  npm run pack:release
25
25
  # On the other machine:
26
- npm install --global ./strckro-0.2.0.tgz
26
+ npm install --global ./strckro-0.3.0.tgz
27
27
  ```
28
28
 
29
29
  ดู [เอกสารฉบับเต็ม](DOCUMENTATION.md) และ [ขั้นตอน publish](PUBLISHING.md)
@@ -63,7 +63,8 @@ Windows เก็บ credential ด้วย DPAPI ผูกกับ OS user; L
63
63
  ## โปรเจคและ deploy
64
64
 
65
65
  ```sh
66
- skctl projects list --json
66
+ skctl projects list
67
+ skctl project my-app --info
67
68
  skctl init --project my-app
68
69
  skctl projects status --json
69
70
  skctl deployments list --json
@@ -76,8 +77,12 @@ skctl rollback --release RELEASE_ID
76
77
  skctl jobs status JOB_ID
77
78
  ```
78
79
 
80
+ `projects list` แสดงตารางหนึ่งบรรทัดต่อโปรเจค มี slug, ประเภทงาน, runtime และสถานะ deployment/process ดูรายละเอียดด้วย `project [SLUG] --info` หรือใช้โปรเจคจาก `--project`, environment และ `strckro.json` เมื่อไม่ระบุ slug `project [SLUG]` แสดงตารางเฉพาะโปรเจคนั้น `--json` ยังคงคืนข้อมูล API ครบสำหรับ AI/CI ทั้ง list และรายละเอียด และ `projects status [SLUG]` ยังใช้ได้ตามเดิม
81
+
79
82
  `init` สร้าง `strckro.json` เก็บแค่ server/project ไม่มี token ไม่เขียนทับไฟล์เดิม อาจ commit ให้ทีมใช้ร่วมกันได้ ลำดับเลือก server: `--server`, `STRCKRO_SERVER`, `strckro.json`, credential ที่บันทึก; project: `--project`, positional slug, `STRCKRO_PROJECT`, `strckro.json`
80
83
 
84
+ `strckro.json` เป็นไฟล์ใน repository ที่อาจมาจากคนอื่น จึงไม่ถูกใช้เป็นปลายทางของ token ที่คุณให้เอง (`STRCKRO_TOKEN`, `login --token`, `--token-stdin`): token จะส่งเฉพาะเมื่อ server มาจาก `--server`, `STRCKRO_SERVER` หรือ origin ที่ login ไว้บนเครื่องนี้ มิฉะนั้นคำสั่งหยุดด้วย exit 2 โดยไม่ส่งคำขอ ใน CI ให้ตั้ง `STRCKRO_SERVER` คู่กับ `STRCKRO_TOKEN`
85
+
81
86
  สร้างโปรเจคด้วย `project.json` ตาม `POST /api/projects/sync` เช่น:
82
87
 
83
88
  ```json
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "strckro",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Strckro deployment control plane CLI for people, CI, and AI agents",
5
5
  "keywords": [
6
6
  "strckro",
package/src/cli.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { parseArgs } from 'node:util';
1
+ import { parseArgs, stripVTControlCharacters } from 'node:util';
2
2
  import { readFileSync } from 'node:fs';
3
3
  import { readFile, writeFile } from 'node:fs/promises';
4
4
  import { hostname } from 'node:os';
@@ -21,7 +21,8 @@ Authentication:
21
21
 
22
22
  Projects and deployment:
23
23
  init --project SLUG Save server/project to ./strckro.json
24
- projects list
24
+ projects list Compact project overview
25
+ project [SLUG] [--info] Overview or details for one project
25
26
  projects status [SLUG]
26
27
  projects sync --file project.json Create/update and sync a Git project
27
28
  deployments list [SLUG] List deployment history and active jobs
@@ -52,6 +53,8 @@ Global options:
52
53
 
53
54
  Environment: STRCKRO_SERVER, STRCKRO_TOKEN, STRCKRO_PROJECT,
54
55
  STRCKRO_CONFIG_DIR, XDG_CONFIG_HOME
56
+ A token (STRCKRO_TOKEN or login --token) is sent only to --server, STRCKRO_SERVER
57
+ or the server of your saved login, never to one named only by ./strckro.json.
55
58
  Legacy DASHBOARD_* variables and dpctl.json remain supported.
56
59
  Exit codes: 0 success; 1 API/deployment; 2 input; 3 authentication;
57
60
  4 permission denied; 5 stale environment; 6 network/timeout.
@@ -60,12 +63,43 @@ Use --json for AI/CI: stdout contains one JSON object; progress goes to stderr.
60
63
  `;
61
64
 
62
65
  const stringOptions = ['server', 'token', 'project', 'scope', 'name', 'days', 'file', 'domains', 'hostname', 'release', 'revision', 'timeout'];
63
- const booleanOptions = ['json', 'help', 'version', 'no-browser', 'token-stdin', 'stdin', 'empty', 'wait', 'sync'];
66
+ const booleanOptions = ['json', 'help', 'version', 'no-browser', 'token-stdin', 'stdin', 'empty', 'wait', 'sync', 'info'];
64
67
  const options = Object.fromEntries([...stringOptions.map(key => [key, { type: 'string' }]), ...booleanOptions.map(key => [key, { type: 'boolean' }])]);
65
68
  const slug = value => { if (!/^[a-z][a-z0-9-]{0,62}$/.test(value || '')) throw new CliError('Specify a valid project slug with --project or skctl init.'); return value; };
66
69
  const keyName = value => { if (!/^[A-Z_][A-Z0-9_]*$/.test(value || '')) throw new CliError('Specify an uppercase environment key.'); return value; };
67
70
  const required = (value, flag) => { if (!value) throw new CliError(`Provide ${flag}.`); return value; };
68
71
 
72
+ // Keep each table entry on one line, even when a server field contains control characters.
73
+ const display = value => stripVTControlCharacters(String(value ?? '')).replace(/[\u0000-\u001f\u007f-\u009f]/g, ' ').replace(/\s+/g, ' ').trim() || '-';
74
+
75
+ function projectList(projects) {
76
+ if (!projects.length) return 'No projects found.\n';
77
+ const rows = [
78
+ ['SLUG', 'TYPE', 'RUNTIME', 'DEPLOYMENT', 'PROCESS'],
79
+ ...projects.map(item => [item.slug, item.serviceType || 'web', item.runtime, item.deployment?.state, item.runtimeStatus?.state || 'unknown'].map(display))
80
+ ];
81
+ const widths = rows[0].map((_, index) => Math.max(...rows.map(row => row[index].length)));
82
+ return rows.map(row => row.map((value, index) => value.padEnd(widths[index])).join(' ').trimEnd()).join('\n')
83
+ + `\n\n${projects.length} project${projects.length === 1 ? '' : 's'}. Details: skctl project <slug> --info\n`;
84
+ }
85
+
86
+ function projectInfo(item) {
87
+ const rows = [
88
+ ['Name', item.name], ['Slug', item.slug], ['Type', item.serviceType || 'web'], ['Runtime', item.runtime],
89
+ ['Deployment', item.deployment?.state], ['Process', item.runtimeStatus?.state || 'unknown'],
90
+ ['Process checked', item.runtimeStatus?.checkedAt],
91
+ ['Active release', item.deployment?.activeReleaseId], ['Previous release', item.deployment?.previousReleaseId],
92
+ ['Deployment updated', item.deployment?.updatedAt],
93
+ ['Deploy job', item.deploymentProgress?.id], ['Job status', item.deploymentProgress?.status], ['Job phase', item.deploymentProgress?.phase],
94
+ ['Repository', item.repository], ['Branch', item.branch], ['Directory', item.directory],
95
+ ['Source sync', item.sync?.status], ['Source synced', item.sync?.at],
96
+ ['Domains', item.domains?.hosts?.join(', ') || '(none)']
97
+ ];
98
+ const width = Math.max(...rows.map(([label]) => label.length));
99
+ return rows.map(([label, value]) => `${label.padEnd(width)} ${display(value)}`).join('\n')
100
+ + '\n\nUse --json for the complete project data.\n';
101
+ }
102
+
69
103
  async function stdinText(input) {
70
104
  if (input.isTTY) throw new CliError('Pipe the value into stdin.');
71
105
  const chunks = []; let size = 0;
@@ -116,9 +150,10 @@ export async function run(argv, { env = process.env, cwd = process.cwd(), stdout
116
150
  const [command, action, ...rest] = args;
117
151
  // Logout does not need a working server or credential decryption.
118
152
  if (command === 'logout') { if (args.length !== 1) throw new CliError('Usage: skctl logout'); await clearConfig(env); print({ ok: true, message: 'Saved credential removed. Revoke it in Portal Settings > Account to invalidate it.' }); return 0; }
119
- const supported = ['login', 'whoami', 'init', 'projects', 'deployments', 'deploy', 'restart', 'rollback', 'jobs', 'logs', 'domains', 'env'];
153
+ const supported = ['login', 'whoami', 'init', 'project', 'projects', 'deployments', 'deploy', 'restart', 'rollback', 'jobs', 'logs', 'domains', 'env'];
120
154
  if (!supported.includes(command)) throw new CliError('Unknown command. Run skctl --help.');
121
155
  const commandOptions = command === 'login' ? ['token', 'token-stdin', 'no-browser', 'scope', 'name', 'days']
156
+ : command === 'project' ? ['info']
122
157
  : command === 'projects' && action === 'sync' ? ['file']
123
158
  : command === 'deploy' ? ['sync', 'wait']
124
159
  : command === 'rollback' ? ['release']
@@ -126,10 +161,18 @@ export async function run(argv, { env = process.env, cwd = process.cwd(), stdout
126
161
  : command === 'env' ? action === 'set' ? ['stdin', 'file', 'empty', 'revision'] : action === 'unset' ? ['revision'] : action === 'import' ? ['file', 'revision'] : action === 'pull' ? ['file'] : [] : [];
127
162
  for (const flag of Object.keys(flags)) if (!['server', 'project', 'json', 'timeout', 'help', 'version', ...commandOptions].includes(flag)) throw new CliError(`--${flag} does not apply to this command.`);
128
163
  const local = await localProject(cwd);
129
- const requestedServer = flags.server || env.DASHBOARD_SERVER || local.server;
164
+ const explicitServer = flags.server || env.DASHBOARD_SERVER;
165
+ const requestedServer = explicitServer || local.server;
130
166
  const needsCredential = command !== 'login' && !env.DASHBOARD_TOKEN;
131
- let saved = !requestedServer || needsCredential ? await readConfig(env, { secret: false }) : {};
167
+ // A token the person supplies (STRCKRO_TOKEN, or login --token/--token-stdin) is a bearer
168
+ // secret, and a login, even by browser approval, ends by replacing the credential saved on this
169
+ // machine. strckro.json is repository content, possibly from a stranger, so it must not pick
170
+ // the server for either: only --server, STRCKRO_SERVER or the origin already logged in to on
171
+ // this machine may be trusted with them.
172
+ const needsTrustedServer = command === 'login' || Boolean(env.DASHBOARD_TOKEN);
173
+ let saved = !requestedServer || needsCredential || (needsTrustedServer && !explicitServer) ? await readConfig(env, { secret: false }) : {};
132
174
  const server = normalizeServer(requestedServer || saved.server);
175
+ if (needsTrustedServer && !explicitServer && saved.server !== server) throw new CliError(`Refusing to use ${server}: that server comes only from strckro.json, which is repository content, so no token or login is sent there. Pass --server or set STRCKRO_SERVER to the Portal you trust.`);
133
176
  if (needsCredential && saved.server === server) saved = await readConfig(env);
134
177
  const timeout = flags.timeout === undefined ? 30 : Number(flags.timeout);
135
178
  if (!Number.isFinite(timeout) || timeout < 1 || timeout > 3600) throw new CliError('Timeout must be 1–3600 seconds.');
@@ -185,6 +228,12 @@ export async function run(argv, { env = process.env, cwd = process.cwd(), stdout
185
228
  if (!projects.some(item => item.slug === target)) throw new CliError('Project not found.', 'API_ERROR', 404);
186
229
  await writeFile(resolve(cwd, 'strckro.json'), JSON.stringify({ server, project: target }, null, 2) + '\n', { flag: 'wx' });
187
230
  result = { ok: true, server, project: target, file: resolve(cwd, 'strckro.json') };
231
+ } else if (command === 'project') {
232
+ if (args.length > 2) throw new CliError('Usage: skctl project [SLUG] [--info]');
233
+ const target = project(action);
234
+ const { projects } = await api('/api/projects');
235
+ result = { project: projects.find(item => item.slug === target) };
236
+ if (!result.project) throw new CliError('Project not found.', 'API_ERROR', 404);
188
237
  } else if (command === 'projects') {
189
238
  if (!['list', 'status', 'sync'].includes(action) || rest.length > (action === 'status' ? 1 : 0)) throw new CliError('Usage: skctl projects list|status [SLUG]|sync --file project.json');
190
239
  if (action === 'sync') {
@@ -259,7 +308,9 @@ export async function run(argv, { env = process.env, cwd = process.cwd(), stdout
259
308
  result = await api(endpoint, { method: 'PATCH', body: { revision, set, unset } });
260
309
  }
261
310
  }
262
- print({ ok: true, ...result });
311
+ if (!json && result?.ok !== false && command === 'projects' && action === 'list') stdout.write(projectList(result.projects));
312
+ else if (!json && result?.ok !== false && command === 'project') stdout.write(flags.info ? projectInfo(result.project) : projectList([result.project]));
313
+ else print({ ok: true, ...result });
263
314
  return result?.ok === false ? 1 : 0;
264
315
  } catch (error) {
265
316
  const message = error instanceof CliError ? error.message : error.code === 'EEXIST' ? 'Output file already exists; choose a new path.' : error.code?.startsWith('E') ? 'Could not access the requested file or configuration.' : error.message || 'Command failed.';