@pplethai/cli 0.4.0 → 0.4.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 +1 -1
- package/dist/bin.js +53 -15
- package/package.json +3 -2
- package/dist/api.js +0 -108
- package/dist/cli.js +0 -151
- package/dist/commands/auth.js +0 -32
- package/dist/commands/deploy.js +0 -113
- package/dist/commands/env.js +0 -70
- package/dist/commands/info.js +0 -238
- package/dist/commands/init.js +0 -159
- package/dist/commands/upgrade.js +0 -29
- package/dist/credentials.js +0 -53
- package/dist/deps.js +0 -87
- package/dist/device-flow.js +0 -122
- package/dist/errors.js +0 -16
- package/dist/git.js +0 -127
- package/dist/link.js +0 -59
- package/dist/node-deps.js +0 -107
- package/dist/npm.js +0 -127
- package/dist/scaffold.js +0 -58
- package/dist/session.js +0 -29
- package/dist/skills.js +0 -86
- package/dist/ui.js +0 -12
- package/dist/update.js +0 -142
package/dist/deps.js
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
export const DEFAULT_CONFIG = {
|
|
5
|
-
apiBase: "https://api.ppleth.ai",
|
|
6
|
-
ssoBase: "https://id.peoplesparty.or.th",
|
|
7
|
-
clientId: "382216793067947795",
|
|
8
|
-
repoBase: "https://github.com",
|
|
9
|
-
registryBase: "https://registry.npmjs.org",
|
|
10
|
-
};
|
|
11
|
-
export function platformConfig(env) {
|
|
12
|
-
return {
|
|
13
|
-
apiBase: env.PPLE_API_BASE ?? DEFAULT_CONFIG.apiBase,
|
|
14
|
-
ssoBase: env.PPLE_SSO_BASE ?? DEFAULT_CONFIG.ssoBase,
|
|
15
|
-
clientId: env.PPLE_CLIENT_ID ?? DEFAULT_CONFIG.clientId,
|
|
16
|
-
repoBase: env.PPLE_REPO_BASE ?? DEFAULT_CONFIG.repoBase,
|
|
17
|
-
registryBase: env.PPLE_REGISTRY_BASE ?? DEFAULT_CONFIG.registryBase,
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
/** XDG-style config home, so credentials never land in the app's repo. */
|
|
21
|
-
export function configDir(env, homeDir) {
|
|
22
|
-
const base = env.XDG_CONFIG_HOME?.trim();
|
|
23
|
-
return path.join(base && base !== "" ? base : path.join(homeDir, ".config"), "pple");
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Whether the CLI may check npm for a newer version. `PPLE_NO_UPDATE_CHECK`
|
|
27
|
-
* is the scripted-environment escape hatch; a local config file is the other
|
|
28
|
-
* half of the opt-out, and lives with the check itself.
|
|
29
|
-
*/
|
|
30
|
-
export function updateChecksAllowed(env) {
|
|
31
|
-
const raw = env.PPLE_NO_UPDATE_CHECK?.trim().toLowerCase();
|
|
32
|
-
return !raw || raw === "0" || raw === "false";
|
|
33
|
-
}
|
|
34
|
-
/** The CLI's own package directory — one above `src/` in a checkout, `dist/` once built. */
|
|
35
|
-
export function packageRoot() {
|
|
36
|
-
return path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* The version this CLI was published as, read from its own `package.json`.
|
|
40
|
-
* A checkout mid-build can be missing or unreadable — an unknown version
|
|
41
|
-
* simply means upgrade treats anything on npm as newer.
|
|
42
|
-
*/
|
|
43
|
-
export function packageVersion() {
|
|
44
|
-
try {
|
|
45
|
-
const manifest = readFileSync(path.join(packageRoot(), "package.json"), "utf8");
|
|
46
|
-
return String(JSON.parse(manifest).version ?? "0.0.0");
|
|
47
|
-
}
|
|
48
|
-
catch {
|
|
49
|
-
return "0.0.0";
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* The template ships inside the published package (`npm run build:template`
|
|
54
|
-
* copies it there); running from a checkout, it is the repo's own directory.
|
|
55
|
-
*/
|
|
56
|
-
export function resolveTemplateDir(env) {
|
|
57
|
-
const override = env.PPLE_TEMPLATE_DIR?.trim();
|
|
58
|
-
if (override)
|
|
59
|
-
return override;
|
|
60
|
-
const bundled = path.join(packageRoot(), "templates", "golden-path");
|
|
61
|
-
if (existsSync(bundled))
|
|
62
|
-
return bundled;
|
|
63
|
-
// Running from a checkout of the platform repo, before `build:template`.
|
|
64
|
-
return path.join(path.dirname(packageRoot()), "template");
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* Platform Skills ship inside the published package (`npm run build:skills`
|
|
68
|
-
* copies them there); running from a checkout, they are the repo's own
|
|
69
|
-
* `skills/pple-platform/`.
|
|
70
|
-
*/
|
|
71
|
-
export function resolveSkillsDir(env) {
|
|
72
|
-
const override = env.PPLE_SKILLS_DIR?.trim();
|
|
73
|
-
if (override)
|
|
74
|
-
return override;
|
|
75
|
-
const bundled = path.join(packageRoot(), "skills", "pple-platform");
|
|
76
|
-
if (existsSync(bundled))
|
|
77
|
-
return bundled;
|
|
78
|
-
return path.join(path.dirname(packageRoot()), "skills", "pple-platform");
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* The helper GIT_ASKPASS points at, which feeds git the app's repo token.
|
|
82
|
-
* git execs it directly rather than through a shell, so it ships with its
|
|
83
|
-
* executable bit set and finds node from its own shebang.
|
|
84
|
-
*/
|
|
85
|
-
export function askpassHelper() {
|
|
86
|
-
return path.join(packageRoot(), "askpass", "pple-askpass.mjs");
|
|
87
|
-
}
|
package/dist/device-flow.js
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
// OAuth 2.0 device authorization grant (RFC 8628) against PPLE ID.
|
|
2
|
-
//
|
|
3
|
-
// A terminal cannot receive a redirect, so the CLI asks PPLE ID for a code,
|
|
4
|
-
// the Builder approves it in a browser (on any device), and the CLI polls
|
|
5
|
-
// until the token is issued. Nothing sensitive is ever typed into the
|
|
6
|
-
// terminal, and the CLI never handles the Builder's password.
|
|
7
|
-
import { CliError } from "./errors.js";
|
|
8
|
-
/** Zitadel's OAuth endpoints; the userinfo path the Provisioner uses too. */
|
|
9
|
-
const DEVICE_AUTHORIZATION_PATH = "/oauth/v2/device_authorization";
|
|
10
|
-
const TOKEN_PATH = "/oauth/v2/token";
|
|
11
|
-
const DEVICE_CODE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
|
12
|
-
/** `offline_access` is what makes a refresh token — and a durable login. */
|
|
13
|
-
const SCOPES = "openid profile offline_access";
|
|
14
|
-
function form(fields) {
|
|
15
|
-
return {
|
|
16
|
-
method: "POST",
|
|
17
|
-
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
18
|
-
body: new URLSearchParams(fields).toString(),
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Name the host we failed to reach. "Check your internet connection" sends a
|
|
23
|
-
* Builder to their wifi when the real fault is a bad `ssoBase` — the host it
|
|
24
|
-
* couldn't reach is the one fact that tells the two apart.
|
|
25
|
-
*/
|
|
26
|
-
function unreachable(ssoBase) {
|
|
27
|
-
return new CliError(`เชื่อมต่อ PPLE ID (${ssoBase}) เพื่อเข้าสู่ระบบไม่ได้`, "ตรวจสอบการเชื่อมต่ออินเทอร์เน็ตแล้วลอง `pple auth login` อีกครั้ง");
|
|
28
|
-
}
|
|
29
|
-
export async function requestDeviceCode(deps) {
|
|
30
|
-
const { ssoBase, clientId } = deps.config;
|
|
31
|
-
const res = await deps
|
|
32
|
-
.fetch(`${ssoBase}${DEVICE_AUTHORIZATION_PATH}`, form({ client_id: clientId, scope: SCOPES }))
|
|
33
|
-
.catch(() => null);
|
|
34
|
-
if (!res || !res.ok)
|
|
35
|
-
throw unreachable(ssoBase);
|
|
36
|
-
const body = (await res.json().catch(() => ({})));
|
|
37
|
-
if (!body.device_code || !body.user_code || !body.verification_uri) {
|
|
38
|
-
throw unreachable(ssoBase);
|
|
39
|
-
}
|
|
40
|
-
return {
|
|
41
|
-
deviceCode: body.device_code,
|
|
42
|
-
userCode: body.user_code,
|
|
43
|
-
verificationUri: body.verification_uri,
|
|
44
|
-
verificationUriComplete: body.verification_uri_complete ?? null,
|
|
45
|
-
expiresInMs: (body.expires_in ?? 600) * 1000,
|
|
46
|
-
// RFC 8628 says 5 seconds when the server doesn't specify.
|
|
47
|
-
intervalMs: (body.interval ?? 5) * 1000,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Poll until the Builder approves in the browser. Backs off when told to
|
|
52
|
-
* (`slow_down`), and gives up when the code expires rather than looping
|
|
53
|
-
* forever in front of someone who walked away.
|
|
54
|
-
*/
|
|
55
|
-
export async function awaitToken(deps, device) {
|
|
56
|
-
const { ssoBase, clientId } = deps.config;
|
|
57
|
-
const deadline = deps.now() + device.expiresInMs;
|
|
58
|
-
let intervalMs = device.intervalMs;
|
|
59
|
-
while (deps.now() < deadline) {
|
|
60
|
-
await deps.sleep(intervalMs);
|
|
61
|
-
const res = await deps
|
|
62
|
-
.fetch(`${ssoBase}${TOKEN_PATH}`, form({
|
|
63
|
-
client_id: clientId,
|
|
64
|
-
grant_type: DEVICE_CODE_GRANT,
|
|
65
|
-
device_code: device.deviceCode,
|
|
66
|
-
}))
|
|
67
|
-
.catch(() => null);
|
|
68
|
-
if (!res)
|
|
69
|
-
throw unreachable(ssoBase);
|
|
70
|
-
const body = (await res.json().catch(() => ({})));
|
|
71
|
-
if (res.ok && body.access_token)
|
|
72
|
-
return toTokenSet(deps, body);
|
|
73
|
-
switch (body.error) {
|
|
74
|
-
case "authorization_pending":
|
|
75
|
-
break;
|
|
76
|
-
case "slow_down":
|
|
77
|
-
intervalMs += 5000;
|
|
78
|
-
break;
|
|
79
|
-
case "access_denied":
|
|
80
|
-
throw new CliError("การเข้าสู่ระบบถูกปฏิเสธในเบราว์เซอร์", "หากไม่ใช่คุณที่ปฏิเสธ ให้รัน `pple auth login` อีกครั้ง");
|
|
81
|
-
case "expired_token":
|
|
82
|
-
throw expiredCode();
|
|
83
|
-
default:
|
|
84
|
-
throw new CliError("PPLE ID ปฏิเสธการเข้าสู่ระบบ", body.error_description ?? "ลอง `pple auth login` อีกครั้ง");
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
throw expiredCode();
|
|
88
|
-
}
|
|
89
|
-
function expiredCode() {
|
|
90
|
-
return new CliError("รหัสเข้าสู่ระบบหมดอายุก่อนได้รับการอนุมัติ", "รัน `pple auth login` อีกครั้ง — ขั้นตอนในเบราว์เซอร์ต้องทำให้เสร็จภายในไม่กี่นาที");
|
|
91
|
-
}
|
|
92
|
-
/** Trade a refresh token for a fresh access token; null if it no longer works. */
|
|
93
|
-
export async function refresh(deps, refreshToken) {
|
|
94
|
-
const { ssoBase, clientId } = deps.config;
|
|
95
|
-
const res = await deps
|
|
96
|
-
.fetch(`${ssoBase}${TOKEN_PATH}`, form({
|
|
97
|
-
client_id: clientId,
|
|
98
|
-
grant_type: "refresh_token",
|
|
99
|
-
refresh_token: refreshToken,
|
|
100
|
-
scope: SCOPES,
|
|
101
|
-
}))
|
|
102
|
-
.catch(() => null);
|
|
103
|
-
if (!res || !res.ok)
|
|
104
|
-
return null;
|
|
105
|
-
const body = (await res.json().catch(() => ({})));
|
|
106
|
-
if (!body.access_token)
|
|
107
|
-
return null;
|
|
108
|
-
return {
|
|
109
|
-
...toTokenSet(deps, body),
|
|
110
|
-
// Rotation is optional: keep the current token when none comes back.
|
|
111
|
-
refreshToken: body.refresh_token ?? refreshToken,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
function toTokenSet(deps, body) {
|
|
115
|
-
return {
|
|
116
|
-
accessToken: body.access_token,
|
|
117
|
-
refreshToken: body.refresh_token ?? null,
|
|
118
|
-
expiresAt: typeof body.expires_in === "number"
|
|
119
|
-
? deps.now() + body.expires_in * 1000
|
|
120
|
-
: null,
|
|
121
|
-
};
|
|
122
|
-
}
|
package/dist/errors.js
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Every failure a Builder can see is a CliError: a sentence about their app,
|
|
3
|
-
* optionally followed by the one thing to do next. Anything else escaping to
|
|
4
|
-
* the top level is a bug in the CLI and is reported as such.
|
|
5
|
-
*/
|
|
6
|
-
export class CliError extends Error {
|
|
7
|
-
hint;
|
|
8
|
-
constructor(message, hint) {
|
|
9
|
-
super(message);
|
|
10
|
-
this.hint = hint;
|
|
11
|
-
this.name = "CliError";
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
/** Both ends of the sign-in — the stored token, and what the platform says. */
|
|
15
|
-
export const SIGNED_OUT = new CliError("คุณยังไม่ได้เข้าสู่ระบบ", "รัน `pple auth login` ก่อน");
|
|
16
|
-
export const SIGN_IN_EXPIRED = new CliError("การเข้าสู่ระบบของคุณหมดอายุแล้ว", "รัน `pple auth login` เพื่อเข้าสู่ระบบอีกครั้ง");
|
package/dist/git.js
DELETED
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
// Git, masked.
|
|
2
|
-
//
|
|
3
|
-
// Builders are not expected to know git, so nothing above this module names
|
|
4
|
-
// it: `pple init` "sets up the app's code", `pple deploy` "sends changes".
|
|
5
|
-
// Every failure that a Builder could plausibly hit is translated here; the
|
|
6
|
-
// raw git output only survives in the hint, where it helps whoever is asked
|
|
7
|
-
// for help next.
|
|
8
|
-
import { CliError } from "./errors.js";
|
|
9
|
-
/** Builder Apps deploy from main; the workflow builds nothing else. */
|
|
10
|
-
export const MAIN_BRANCH = "main";
|
|
11
|
-
/** The username half of the Basic credential; GitHub ignores its value. */
|
|
12
|
-
const TOKEN_USERNAME = "x-access-token";
|
|
13
|
-
/**
|
|
14
|
-
* Every git invocation runs with the app's token in its environment and none
|
|
15
|
-
* of the machine's own credentials in play:
|
|
16
|
-
*
|
|
17
|
-
* - `GIT_ASKPASS` is how the token reaches git. Passing it in the URL would
|
|
18
|
-
* put it in argv, which `ps` shows to every user on the machine, and in
|
|
19
|
-
* `.git/config` once git wrote the remote down.
|
|
20
|
-
* - `credential.helper=` empties the helper list, so a keychain entry left
|
|
21
|
-
* over from someone's own GitHub account can't answer ahead of the platform.
|
|
22
|
-
* - `GIT_TERMINAL_PROMPT=0` stops git asking a Builder for a password it is
|
|
23
|
-
* the platform's job to supply.
|
|
24
|
-
*/
|
|
25
|
-
async function git(deps, dir, args, access) {
|
|
26
|
-
return deps.run("git", access ? ["-c", "credential.helper=", ...args] : args, {
|
|
27
|
-
cwd: dir,
|
|
28
|
-
env: {
|
|
29
|
-
GIT_TERMINAL_PROMPT: "0",
|
|
30
|
-
...(access
|
|
31
|
-
? {
|
|
32
|
-
GIT_ASKPASS: deps.gitAskpass,
|
|
33
|
-
PPLE_GIT_USER: TOKEN_USERNAME,
|
|
34
|
-
PPLE_GIT_TOKEN: access.token,
|
|
35
|
-
}
|
|
36
|
-
: {}),
|
|
37
|
-
},
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
/** Author every commit as the Builder, since the pusher is the platform's bot. */
|
|
41
|
-
function authorArgs(access) {
|
|
42
|
-
return [
|
|
43
|
-
"-c",
|
|
44
|
-
`user.name=${access.author.name}`,
|
|
45
|
-
"-c",
|
|
46
|
-
`user.email=${access.author.email}`,
|
|
47
|
-
];
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* git failed because it was not allowed near the app's repo. The token is the
|
|
51
|
-
* platform's to mint, so this is the platform having declined — not something
|
|
52
|
-
* a Builder fixes with a credential of their own.
|
|
53
|
-
*/
|
|
54
|
-
function isAccessFailure(result) {
|
|
55
|
-
return /authentication|could not read|403|denied|not found/i.test(`${result.stdout}\n${result.stderr}`);
|
|
56
|
-
}
|
|
57
|
-
function noAccess(action, command) {
|
|
58
|
-
return new CliError(`แพลตฟอร์มไม่อนุญาตให้เครื่องนี้${action}`, `ตรวจสอบว่าแอปยังเป็นของคุณอยู่ที่ https://console.ppleth.ai แล้วรัน \`${command}\` อีกครั้ง`);
|
|
59
|
-
}
|
|
60
|
-
function unreadableFolder(result) {
|
|
61
|
-
return new CliError("อ่านโฟลเดอร์ของแอปไม่ได้", result.stderr.trim());
|
|
62
|
-
}
|
|
63
|
-
async function gitOrThrow(deps, dir, args, onFailure, access) {
|
|
64
|
-
const result = await git(deps, dir, args, access).catch(() => {
|
|
65
|
-
throw new CliError("เครื่องนี้ยังไม่มีเครื่องมือที่ PPLE CLI ต้องใช้", "ติดตั้ง git (https://git-scm.com/downloads) แล้วรันคำสั่งอีกครั้ง");
|
|
66
|
-
});
|
|
67
|
-
if (result.code !== 0)
|
|
68
|
-
throw onFailure(result);
|
|
69
|
-
return result;
|
|
70
|
-
}
|
|
71
|
-
/** Fetch an app's code into `targetDir`, which must not exist yet. */
|
|
72
|
-
export async function cloneApp(deps, repoUrl, targetDir, access) {
|
|
73
|
-
await gitOrThrow(deps, deps.cwd, ["clone", repoUrl, targetDir], (result) => isAccessFailure(result)
|
|
74
|
-
? noAccess("เข้าถึงโค้ดของแอปคุณ", "pple init")
|
|
75
|
-
: new CliError("ดึงโค้ดของแอปไม่สำเร็จ", result.stderr.trim()), access);
|
|
76
|
-
}
|
|
77
|
-
/** True when the working copy holds changes that haven't been recorded yet. */
|
|
78
|
-
export async function hasLocalChanges(deps, dir) {
|
|
79
|
-
const result = await gitOrThrow(deps, dir, ["status", "--porcelain"], unreadableFolder);
|
|
80
|
-
return result.stdout.trim() !== "";
|
|
81
|
-
}
|
|
82
|
-
/** Record every change in the folder as one entry in the app's history. */
|
|
83
|
-
export async function recordChanges(deps, dir, message, access) {
|
|
84
|
-
await gitOrThrow(deps, dir, ["add", "-A"], unreadableFolder);
|
|
85
|
-
await gitOrThrow(deps, dir, [...authorArgs(access), "commit", "-m", message], (r) => new CliError("บันทึกการเปลี่ยนแปลงของคุณไม่สำเร็จ", r.stderr.trim()));
|
|
86
|
-
}
|
|
87
|
-
export async function headCommit(deps, dir) {
|
|
88
|
-
const result = await gitOrThrow(deps, dir, ["rev-parse", "HEAD"], (r) => new CliError("อ่านประวัติของแอปไม่สำเร็จ", r.stderr.trim()));
|
|
89
|
-
return result.stdout.trim();
|
|
90
|
-
}
|
|
91
|
-
/** True when the local branch already matches what the platform has. */
|
|
92
|
-
export async function isUpToDateWithRemote(deps, dir, access) {
|
|
93
|
-
await git(deps, dir, ["fetch", "origin", MAIN_BRANCH], access);
|
|
94
|
-
const local = await git(deps, dir, ["rev-parse", "HEAD"]);
|
|
95
|
-
const remote = await git(deps, dir, ["rev-parse", `origin/${MAIN_BRANCH}`]);
|
|
96
|
-
if (local.code !== 0 || remote.code !== 0)
|
|
97
|
-
return false;
|
|
98
|
-
return local.stdout.trim() === remote.stdout.trim();
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* Send the recorded changes to the platform. When the app has changes this
|
|
102
|
-
* copy hasn't seen — a Collaborator deployed meanwhile — they are merged in
|
|
103
|
-
* first, and only a genuine conflict reaches the Builder.
|
|
104
|
-
*/
|
|
105
|
-
export async function sendChanges(deps, dir, access) {
|
|
106
|
-
const push = ["push", "origin", `HEAD:${MAIN_BRANCH}`];
|
|
107
|
-
const pushed = await git(deps, dir, push, access);
|
|
108
|
-
if (pushed.code === 0)
|
|
109
|
-
return;
|
|
110
|
-
const output = `${pushed.stdout}\n${pushed.stderr}`;
|
|
111
|
-
if (!/rejected|non-fast-forward|fetch first/i.test(output)) {
|
|
112
|
-
if (isAccessFailure(pushed)) {
|
|
113
|
-
throw noAccess("ส่งการเปลี่ยนแปลงไปยังแอปของคุณ", "pple deploy");
|
|
114
|
-
}
|
|
115
|
-
throw new CliError("ส่งการเปลี่ยนแปลงของคุณไม่สำเร็จ", pushed.stderr.trim());
|
|
116
|
-
}
|
|
117
|
-
deps.out.say("มีคนอื่นแก้ไขแอปนี้ — กำลังนำงานของเขาเข้ามาก่อน…");
|
|
118
|
-
const rebase = await git(deps, dir, [...authorArgs(access), "pull", "--rebase", "origin", MAIN_BRANCH], access);
|
|
119
|
-
if (rebase.code !== 0) {
|
|
120
|
-
await git(deps, dir, ["rebase", "--abort"]);
|
|
121
|
-
throw new CliError("การเปลี่ยนแปลงของคุณขัดแย้งกับที่คนอื่นดีพลอยไปแล้ว", "ให้ AI coding agent ของคุณรวมสองเวอร์ชันเข้าด้วยกัน แล้วรัน `pple deploy` อีกครั้ง");
|
|
122
|
-
}
|
|
123
|
-
const retried = await git(deps, dir, push, access);
|
|
124
|
-
if (retried.code !== 0) {
|
|
125
|
-
throw new CliError("ส่งการเปลี่ยนแปลงของคุณไม่สำเร็จ", retried.stderr.trim());
|
|
126
|
-
}
|
|
127
|
-
}
|
package/dist/link.js
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
// The link between a folder on a Builder's computer and an app on the
|
|
2
|
-
// platform. `pple init` writes it, `pple deploy` reads it — which is why
|
|
3
|
-
// deploy needs no arguments and cannot ship the wrong app.
|
|
4
|
-
import fs from "node:fs/promises";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import { CliError } from "./errors.js";
|
|
7
|
-
export const LINK_DIR = ".pple";
|
|
8
|
-
export const LINK_FILE = "app.json";
|
|
9
|
-
export async function writeLink(dir, link) {
|
|
10
|
-
await fs.mkdir(path.join(dir, LINK_DIR), { recursive: true });
|
|
11
|
-
await fs.writeFile(path.join(dir, LINK_DIR, LINK_FILE), `${JSON.stringify(link, null, 2)}\n`);
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* Find the app this command is about by walking up from the current folder,
|
|
15
|
-
* so `pple deploy` works from anywhere inside the app.
|
|
16
|
-
*/
|
|
17
|
-
export async function findLink(deps) {
|
|
18
|
-
const found = await peekLink(deps);
|
|
19
|
-
if (found)
|
|
20
|
-
return found;
|
|
21
|
-
throw new CliError("โฟลเดอร์นี้ไม่ใช่แอป PPLE", "เข้าไปในโฟลเดอร์ของแอป หรือรัน `pple init <app>` เพื่อตั้งค่าแอปใหม่");
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Like `findLink`, but returns null when no App Link is nearby — used by
|
|
25
|
-
* `pple init` to refuse creating a second app inside an already-linked folder.
|
|
26
|
-
*/
|
|
27
|
-
export async function peekLink(deps) {
|
|
28
|
-
let dir = path.resolve(deps.cwd);
|
|
29
|
-
for (;;) {
|
|
30
|
-
const candidate = path.join(dir, LINK_DIR, LINK_FILE);
|
|
31
|
-
const raw = await fs.readFile(candidate, "utf8").catch(() => null);
|
|
32
|
-
if (raw !== null) {
|
|
33
|
-
const link = parseLink(raw);
|
|
34
|
-
if (link)
|
|
35
|
-
return { ...link, dir };
|
|
36
|
-
throw new CliError("การเชื่อมโยงแอปนี้กับแพลตฟอร์มเสียหาย", "รัน `pple init <app>` ในโฟลเดอร์ใหม่เพื่อตั้งค่าอีกครั้ง");
|
|
37
|
-
}
|
|
38
|
-
const parent = path.dirname(dir);
|
|
39
|
-
if (parent === dir)
|
|
40
|
-
break;
|
|
41
|
-
dir = parent;
|
|
42
|
-
}
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
function parseLink(raw) {
|
|
46
|
-
try {
|
|
47
|
-
const parsed = JSON.parse(raw);
|
|
48
|
-
if (typeof parsed.slug !== "string" || parsed.slug === "")
|
|
49
|
-
return null;
|
|
50
|
-
return {
|
|
51
|
-
slug: parsed.slug,
|
|
52
|
-
apiBase: typeof parsed.apiBase === "string" ? parsed.apiBase : "",
|
|
53
|
-
url: typeof parsed.url === "string" ? parsed.url : "",
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
catch {
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
|
-
}
|
package/dist/node-deps.js
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
// The Node implementations behind `Deps` — the only part of the CLI that
|
|
2
|
-
// touches the real filesystem, the real clock, and real subprocesses.
|
|
3
|
-
import { spawn } from "node:child_process";
|
|
4
|
-
import readline from "node:readline/promises";
|
|
5
|
-
import { askpassHelper, configDir, packageRoot, packageVersion, platformConfig, resolveSkillsDir, resolveTemplateDir, updateChecksAllowed, } from "./deps.js";
|
|
6
|
-
export function spawnRun(command, args, options) {
|
|
7
|
-
return new Promise((resolve, reject) => {
|
|
8
|
-
const child = spawn(command, args, {
|
|
9
|
-
cwd: options.cwd,
|
|
10
|
-
env: { ...process.env, ...options.env },
|
|
11
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
12
|
-
});
|
|
13
|
-
let stdout = "";
|
|
14
|
-
let stderr = "";
|
|
15
|
-
child.stdout.on("data", (chunk) => (stdout += String(chunk)));
|
|
16
|
-
child.stderr.on("data", (chunk) => (stderr += String(chunk)));
|
|
17
|
-
child.on("error", reject);
|
|
18
|
-
child.on("close", (code) => resolve({ code: code ?? 0, stdout, stderr }));
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
|
-
/** Best-effort browser launch: login still works from the printed URL. */
|
|
22
|
-
async function openBrowser(url) {
|
|
23
|
-
const command = process.platform === "darwin"
|
|
24
|
-
? "open"
|
|
25
|
-
: process.platform === "win32"
|
|
26
|
-
? "start"
|
|
27
|
-
: "xdg-open";
|
|
28
|
-
await spawnRun(command, [url], { cwd: process.cwd() }).catch(() => undefined);
|
|
29
|
-
}
|
|
30
|
-
/** A yes/no question at the terminal. Anything but yes means no. */
|
|
31
|
-
async function confirm(question) {
|
|
32
|
-
const rl = readline.createInterface({
|
|
33
|
-
input: process.stdin,
|
|
34
|
-
output: process.stdout,
|
|
35
|
-
});
|
|
36
|
-
try {
|
|
37
|
-
const answer = await rl.question(`${question} [y/N] `);
|
|
38
|
-
return /^y(es)?$/i.test(answer.trim());
|
|
39
|
-
}
|
|
40
|
-
finally {
|
|
41
|
-
rl.close();
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
/** A free-text question at the terminal (App Name, etc.). */
|
|
45
|
-
async function ask(question) {
|
|
46
|
-
const rl = readline.createInterface({
|
|
47
|
-
input: process.stdin,
|
|
48
|
-
output: process.stdout,
|
|
49
|
-
});
|
|
50
|
-
try {
|
|
51
|
-
return await rl.question(`${question} `);
|
|
52
|
-
}
|
|
53
|
-
finally {
|
|
54
|
-
rl.close();
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
/** Drain stdin to EOF — for piping multiline or secret env values. */
|
|
58
|
-
async function readStdin() {
|
|
59
|
-
const chunks = [];
|
|
60
|
-
for await (const chunk of process.stdin) {
|
|
61
|
-
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
62
|
-
}
|
|
63
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* Run the Builder's command line again on the `pple` that is installed now.
|
|
67
|
-
* The child inherits the terminal, so a re-run deploy still prompts and
|
|
68
|
-
* prints as if the Builder had typed it themselves.
|
|
69
|
-
*
|
|
70
|
-
* It re-runs the same script path deliberately: a global install replaces
|
|
71
|
-
* what that path resolves to, so the old path is exactly how to reach the new
|
|
72
|
-
* version — and the path this process was started from is the one the Builder
|
|
73
|
-
* chose, whatever else is on PATH.
|
|
74
|
-
*/
|
|
75
|
-
function reexec(argv) {
|
|
76
|
-
return new Promise((resolve, reject) => {
|
|
77
|
-
const child = spawn(process.execPath, [process.argv[1], ...argv], {
|
|
78
|
-
stdio: "inherit",
|
|
79
|
-
});
|
|
80
|
-
child.on("error", reject);
|
|
81
|
-
child.on("close", (code) => resolve(code ?? 0));
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
export function nodeDeps(out, homeDir) {
|
|
85
|
-
return {
|
|
86
|
-
fetch: globalThis.fetch.bind(globalThis),
|
|
87
|
-
now: () => Date.now(),
|
|
88
|
-
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
89
|
-
cwd: process.cwd(),
|
|
90
|
-
configDir: configDir(process.env, homeDir),
|
|
91
|
-
out,
|
|
92
|
-
openBrowser,
|
|
93
|
-
run: spawnRun,
|
|
94
|
-
templateDir: resolveTemplateDir(process.env),
|
|
95
|
-
skillsDir: resolveSkillsDir(process.env),
|
|
96
|
-
packageDir: packageRoot(),
|
|
97
|
-
version: packageVersion(),
|
|
98
|
-
interactive: process.stdin.isTTY === true && process.stdout.isTTY === true,
|
|
99
|
-
confirm,
|
|
100
|
-
ask,
|
|
101
|
-
readStdin,
|
|
102
|
-
reexec,
|
|
103
|
-
updateChecks: updateChecksAllowed(process.env),
|
|
104
|
-
gitAskpass: askpassHelper(),
|
|
105
|
-
config: platformConfig(process.env),
|
|
106
|
-
};
|
|
107
|
-
}
|
package/dist/npm.js
DELETED
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
// npm, and the only place that names it: what the registry says the latest
|
|
2
|
-
// CLI is, and whether this copy is one npm's global root holds. Everything
|
|
3
|
-
// here answers a question — nothing here talks to the Builder.
|
|
4
|
-
import { realpath } from "node:fs/promises";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
export const PACKAGE = "@pplethai/cli";
|
|
7
|
-
/** The line a Builder can always fall back to, whatever went wrong. */
|
|
8
|
-
export const MANUAL_INSTALL = `npm install -g ${PACKAGE}`;
|
|
9
|
-
/** What npm's `latest` dist-tag points at, or null when npm can't be reached. */
|
|
10
|
-
export async function latestVersion(deps) {
|
|
11
|
-
const response = await deps
|
|
12
|
-
.fetch(`${deps.config.registryBase}/${PACKAGE}/latest`, {
|
|
13
|
-
headers: { accept: "application/json" },
|
|
14
|
-
})
|
|
15
|
-
.catch(() => null);
|
|
16
|
-
if (!response?.ok)
|
|
17
|
-
return null;
|
|
18
|
-
const body = (await response.json().catch(() => null));
|
|
19
|
-
return typeof body?.version === "string" && body.version !== ""
|
|
20
|
-
? body.version
|
|
21
|
-
: null;
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Whether the running CLI is the copy npm's global root holds. Anything else
|
|
25
|
-
* — a Homebrew wrapper, a checkout, another package manager — is a channel
|
|
26
|
-
* the CLI doesn't own and must not install over.
|
|
27
|
-
*/
|
|
28
|
-
export async function isNpmGlobalInstall(deps) {
|
|
29
|
-
const result = await deps
|
|
30
|
-
.run("npm", ["root", "-g"], { cwd: deps.cwd })
|
|
31
|
-
.catch(() => null);
|
|
32
|
-
if (!result || result.code !== 0)
|
|
33
|
-
return false;
|
|
34
|
-
const globalRoot = result.stdout.trim();
|
|
35
|
-
if (globalRoot === "")
|
|
36
|
-
return false;
|
|
37
|
-
return contains(globalRoot, deps.packageDir);
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Install a specific version globally. Never with sudo — see ADR-0007.
|
|
41
|
-
*
|
|
42
|
-
* An npm that can't be started at all (not on PATH, not executable) comes
|
|
43
|
-
* back as a failed run rather than a rejection: to a Builder that is the same
|
|
44
|
-
* event as an install that failed, and it deserves the same explanation
|
|
45
|
-
* instead of a stack trace.
|
|
46
|
-
*/
|
|
47
|
-
export function installGlobally(deps, version) {
|
|
48
|
-
return deps
|
|
49
|
-
.run("npm", ["install", "-g", `${PACKAGE}@${version}`], { cwd: deps.cwd })
|
|
50
|
-
.catch((e) => ({
|
|
51
|
-
code: 1,
|
|
52
|
-
stdout: "",
|
|
53
|
-
stderr: e instanceof Error ? e.message : String(e),
|
|
54
|
-
}));
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Whether `child` sits inside `parent`. Both sides are resolved through
|
|
58
|
-
* symlinks first: a Node install managed by nvm or Volta reaches the CLI by a
|
|
59
|
-
* symlinked path, and comparing the two spellings would call a perfectly
|
|
60
|
-
* ordinary npm-global install "some other channel".
|
|
61
|
-
*/
|
|
62
|
-
async function contains(parent, child) {
|
|
63
|
-
const [from, to] = await Promise.all([resolve(parent), resolve(child)]);
|
|
64
|
-
const relative = path.relative(from, to);
|
|
65
|
-
return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
66
|
-
}
|
|
67
|
-
/** Real path where one exists; the literal path where it doesn't. */
|
|
68
|
-
async function resolve(dir) {
|
|
69
|
-
return realpath(dir).catch(() => path.resolve(dir));
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* Compare two npm versions. Prereleases sort below their own release, dotted
|
|
73
|
-
* prerelease segments compare numerically where both sides are numbers, and
|
|
74
|
-
* anything unparseable counts as older so a broken version never wins.
|
|
75
|
-
*/
|
|
76
|
-
export function isNewer(candidate, current) {
|
|
77
|
-
const a = parseVersion(candidate);
|
|
78
|
-
const b = parseVersion(current);
|
|
79
|
-
if (!a)
|
|
80
|
-
return false;
|
|
81
|
-
if (!b)
|
|
82
|
-
return true;
|
|
83
|
-
for (let i = 0; i < 3; i += 1) {
|
|
84
|
-
if (a.parts[i] !== b.parts[i])
|
|
85
|
-
return a.parts[i] > b.parts[i];
|
|
86
|
-
}
|
|
87
|
-
return comparePrerelease(a.prerelease, b.prerelease) > 0;
|
|
88
|
-
}
|
|
89
|
-
function parseVersion(version) {
|
|
90
|
-
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/.exec(version.trim());
|
|
91
|
-
if (!match)
|
|
92
|
-
return null;
|
|
93
|
-
return {
|
|
94
|
-
parts: [Number(match[1]), Number(match[2]), Number(match[3])],
|
|
95
|
-
prerelease: match[4] ? match[4].split(".") : [],
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
/** Semver's prerelease rules: a release outranks any prerelease of itself. */
|
|
99
|
-
function comparePrerelease(a, b) {
|
|
100
|
-
if (a.length === 0 && b.length === 0)
|
|
101
|
-
return 0;
|
|
102
|
-
if (a.length === 0)
|
|
103
|
-
return 1;
|
|
104
|
-
if (b.length === 0)
|
|
105
|
-
return -1;
|
|
106
|
-
for (let i = 0; i < Math.max(a.length, b.length); i += 1) {
|
|
107
|
-
const left = a[i];
|
|
108
|
-
const right = b[i];
|
|
109
|
-
if (left === undefined)
|
|
110
|
-
return -1;
|
|
111
|
-
if (right === undefined)
|
|
112
|
-
return 1;
|
|
113
|
-
const numeric = /^\d+$/.test(left) && /^\d+$/.test(right);
|
|
114
|
-
if (left === right)
|
|
115
|
-
continue;
|
|
116
|
-
return numeric ? (Number(left) > Number(right) ? 1 : -1) : left > right ? 1 : -1;
|
|
117
|
-
}
|
|
118
|
-
return 0;
|
|
119
|
-
}
|
|
120
|
-
/** The most useful line npm printed, without the noise around it. */
|
|
121
|
-
export function npmMessage(result) {
|
|
122
|
-
const lines = `${result.stderr}\n${result.stdout}`
|
|
123
|
-
.split("\n")
|
|
124
|
-
.map((line) => line.replace(/^npm (ERR!|error|warn)\s*/i, "").trim())
|
|
125
|
-
.filter((line) => line !== "");
|
|
126
|
-
return lines.slice(0, 3).join("\n") || "หยุดทำงานโดยไม่ได้ระบุสาเหตุ";
|
|
127
|
-
}
|